dsh-wsl-desktop 0.2.0 → 0.2.1
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 +1 -1
- package/lib/client.js +19 -6
- package/lib/index.js +29 -17
- package/lib/wsl/__pycache__/terminal-bridge.cpython-312.pyc +0 -0
- package/lib/wsl/confinement.js +5 -1
- package/lib/wsl/dsh-wsl-confine.sh +37 -16
- package/lib/wsl/preset.js +36 -271
- package/lib/wsl/shell.js +6 -1
- package/lib/wsl/terminal-bridge.py +13 -3
- package/lib/wsl/world.js +6 -11
- package/package.json +37 -35
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ harness 在会话**创建**时就把 preset 定下来(`SessionCreateRequest.ag
|
|
|
63
63
|
两个实测前提:
|
|
64
64
|
|
|
65
65
|
- **控制进程必须在桥报告会话之后才启动**,因为在桥创建 FIFO 之前 `open` 会直接失败。桥在 `_spawn()` 前先建 FIFO,靠 `started` 应答把这一点变成可观测的时序。
|
|
66
|
-
- **rc 里向终端发查询的程序会挂住整个会话。**
|
|
66
|
+
- **rc 里向终端发查询的程序会挂住整个会话。** 有的发行版用户的 `~/.bashrc` 末尾会启动向终端发查询的程序(如 `fastfetch`),它们要等只有终端模拟器才会给的应答,而无头校验里没有应答方,于是永远不出提示符。真实 Web 终端里 xterm.js 会应答,所以这是校验环境的问题而不是桥的缺陷 —— 但据此把校验用的 shell 固定为 `bash --noprofile --norc -i`,让校验测的是桥而不是用户的 rc。
|
|
67
67
|
|
|
68
68
|
PTC 的 `stdio.control`(fd 通道)仍然明确拒绝:`wsl.exe` 无法转发任意描述符。
|
|
69
69
|
|
package/lib/client.js
CHANGED
|
@@ -528,7 +528,12 @@ window.__ModuleLoader__.load({
|
|
|
528
528
|
renderedError = state.error
|
|
529
529
|
}
|
|
530
530
|
|
|
531
|
-
teardown
|
|
531
|
+
// The uninstall teardown gets forceClose, not close: a plugin
|
|
532
|
+
// uninstalled mid-commit must still release the overlay, and the token
|
|
533
|
+
// bump makes the in-flight commit's settled result drop instead of
|
|
534
|
+
// creating a workspace on a dead context. The dialog's own cancel paths
|
|
535
|
+
// (overlay click, Escape) keep the guarded close below.
|
|
536
|
+
teardown = forceClose
|
|
532
537
|
document.addEventListener('keydown', onKey)
|
|
533
538
|
hostElement().append(overlay)
|
|
534
539
|
render()
|
|
@@ -552,7 +557,7 @@ window.__ModuleLoader__.load({
|
|
|
552
557
|
}
|
|
553
558
|
|
|
554
559
|
/** The injected services the picker and its trigger read. */
|
|
555
|
-
const companion = { button: null,
|
|
560
|
+
const companion = { button: null, trigger: null, observer: null, scheduled: false }
|
|
556
561
|
|
|
557
562
|
/** Find the shipped "add workspace" trigger by its icon geometry. */
|
|
558
563
|
function findTrigger() {
|
|
@@ -608,10 +613,19 @@ window.__ModuleLoader__.load({
|
|
|
608
613
|
companion.scheduled = false
|
|
609
614
|
const { button } = companion
|
|
610
615
|
if (button === null) return
|
|
611
|
-
|
|
616
|
+
// Steady-state fast path: the cached trigger stays valid while it
|
|
617
|
+
// remains connected inside the trailing action cluster, so chat
|
|
618
|
+
// streaming (a mutation every frame) re-runs only the cheap placement
|
|
619
|
+
// and visibility checks below instead of the whole-document icon scan.
|
|
620
|
+
// The cache self-heals: a replaced or relocated trigger is disconnected
|
|
621
|
+
// and forces one full rescan.
|
|
622
|
+
let trigger = companion.trigger
|
|
623
|
+
if (trigger === null || !trigger.isConnected || !hasHeaderActionsParent(trigger)) {
|
|
624
|
+
trigger = findTrigger()
|
|
625
|
+
companion.trigger = trigger
|
|
626
|
+
}
|
|
612
627
|
if (trigger === null) {
|
|
613
628
|
if (button.isConnected) button.remove()
|
|
614
|
-
companion.after = null
|
|
615
629
|
return
|
|
616
630
|
}
|
|
617
631
|
const target = actionCluster(trigger)
|
|
@@ -622,7 +636,6 @@ window.__ModuleLoader__.load({
|
|
|
622
636
|
button.className = trigger.className
|
|
623
637
|
button.style.display = ''
|
|
624
638
|
target.after(button)
|
|
625
|
-
companion.after = target
|
|
626
639
|
}
|
|
627
640
|
// Measure with the button rendered, then decide. The header row clips its
|
|
628
641
|
// own overflow, and the shipped action cluster hides itself while the
|
|
@@ -693,7 +706,7 @@ window.__ModuleLoader__.load({
|
|
|
693
706
|
observer.disconnect()
|
|
694
707
|
button.remove()
|
|
695
708
|
companion.button = null
|
|
696
|
-
companion.
|
|
709
|
+
companion.trigger = null
|
|
697
710
|
companion.observer = null
|
|
698
711
|
teardown?.()
|
|
699
712
|
}
|
package/lib/index.js
CHANGED
|
@@ -130,9 +130,6 @@ function sourceDirFor(id) {
|
|
|
130
130
|
return undefined
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
/** Disposers of the variants this plugin registered; the plugin owns their lifecycle. */
|
|
134
|
-
const variantDisposers = new Map()
|
|
135
|
-
|
|
136
133
|
/**
|
|
137
134
|
* Read a base preset's declared plugin rows as OBJECTS.
|
|
138
135
|
*
|
|
@@ -159,14 +156,18 @@ function basePluginsOf(presets, id) {
|
|
|
159
156
|
* variant cleanly.
|
|
160
157
|
* @param {import('@deepseek-ai/cordis').Context} ctx - the plugin context.
|
|
161
158
|
* @param {string | undefined} distro - distribution to pin.
|
|
162
|
-
* @param {{ disposed: boolean }} generation - this
|
|
163
|
-
* lifecycle token; the plugin's teardown flips
|
|
164
|
-
* before draining, and the loop re-checks it around
|
|
165
|
-
* disable landing mid-registration cannot orphan a variant
|
|
166
|
-
* nobody owns.
|
|
159
|
+
* @param {{ disposed: boolean, disposers: Map<string, Function> }} generation - this
|
|
160
|
+
* registration generation's lifecycle token; the plugin's teardown flips
|
|
161
|
+
* `disposed` synchronously before draining, and the loop re-checks it around
|
|
162
|
+
* every await so a disable landing mid-registration cannot orphan a variant
|
|
163
|
+
* whose disposer nobody owns. The disposer map lives ON the generation, so
|
|
164
|
+
* one generation's teardown drains only the variants it registered.
|
|
165
|
+
* @param {{ disposers: Map<string, Function>} | null} previous - the generation
|
|
166
|
+
* whose variants must be withdrawn before this one registers (register()
|
|
167
|
+
* refuses duplicates).
|
|
167
168
|
* @returns {Promise<object>} the registration outcome for the route.
|
|
168
169
|
*/
|
|
169
|
-
async function materializePresets(ctx, distro, generation) {
|
|
170
|
+
async function materializePresets(ctx, distro, generation, previous) {
|
|
170
171
|
const presets = ctx.get('agentPresets')
|
|
171
172
|
if (presets === undefined) throw new Error('agentPresets 服务不可用')
|
|
172
173
|
// 版本门控:本插件的目标面是 0.1.7 的运行时注册架构。旧宿主(无
|
|
@@ -183,9 +184,9 @@ async function materializePresets(ctx, distro, generation) {
|
|
|
183
184
|
const skipped = []
|
|
184
185
|
|
|
185
186
|
// Withdraw the previous generation first: register() refuses duplicates.
|
|
186
|
-
for (const [id, dispose] of
|
|
187
|
+
for (const [id, dispose] of previous?.disposers ?? []) {
|
|
187
188
|
try { await dispose() } catch { /* already gone */ }
|
|
188
|
-
|
|
189
|
+
previous?.disposers.delete(id)
|
|
189
190
|
if (generation?.disposed) return { status: 'ready', hostCompat, written: [], skipped: [{ id: '-', reason: '插件已卸载,放弃本轮注册' }] }
|
|
190
191
|
}
|
|
191
192
|
|
|
@@ -236,7 +237,7 @@ async function materializePresets(ctx, distro, generation) {
|
|
|
236
237
|
skipped.push({ id: variantId, reason: '插件已卸载,注册即撤回' })
|
|
237
238
|
continue
|
|
238
239
|
}
|
|
239
|
-
|
|
240
|
+
generation.disposers.set(variantId, dispose)
|
|
240
241
|
written.push({ id: variantId, from: id, removed })
|
|
241
242
|
} catch (error) {
|
|
242
243
|
const message = error instanceof Error ? error.message : String(error)
|
|
@@ -264,9 +265,17 @@ let currentGeneration = null
|
|
|
264
265
|
*/
|
|
265
266
|
async function refreshPresets(ctx, distro, generation = currentGeneration) {
|
|
266
267
|
const run = async () => {
|
|
268
|
+
// A caller may omit the generation (developer-token regenerate before the
|
|
269
|
+
// inject ran): mint a transient one so every disposer has an owner.
|
|
270
|
+
const gen = generation ?? { disposed: false, disposers: new Map() }
|
|
271
|
+
// Withdrawal targets the PREVIOUS generation's disposers; a self-refresh
|
|
272
|
+
// (developer-token regenerate) withdraws its own — the exact behaviour of
|
|
273
|
+
// the historical shared map, minus the cross-generation teardown theft.
|
|
274
|
+
const previous = currentGeneration === gen ? gen : currentGeneration
|
|
275
|
+
currentGeneration = gen
|
|
267
276
|
presetState = { status: 'running' }
|
|
268
277
|
try {
|
|
269
|
-
const result = await materializePresets(ctx, distro,
|
|
278
|
+
const result = await materializePresets(ctx, distro, gen, previous)
|
|
270
279
|
presetState = { status: 'ready', ...result }
|
|
271
280
|
} catch (error) {
|
|
272
281
|
presetState = { status: 'failed', error: error instanceof Error ? error.message : String(error) }
|
|
@@ -975,12 +984,15 @@ export function apply(ctx, config) {
|
|
|
975
984
|
// await re-checks the flag around every register() and hands any
|
|
976
985
|
// just-registered variant straight back to its disposer, so a disable
|
|
977
986
|
// landing mid-registration cannot orphan a wsl-* preset in the registry.
|
|
978
|
-
|
|
979
|
-
|
|
987
|
+
// Per-generation disposer ownership: one generation's teardown drains only
|
|
988
|
+
// the variants ITS registration created, so a second generation mounted
|
|
989
|
+
// beside a running one (the documented same-process exercise path) can no
|
|
990
|
+
// longer have its variants torn down by the first generation's dispose.
|
|
991
|
+
const generation = { disposed: false, disposers: new Map() }
|
|
980
992
|
scope.effect(() => () => {
|
|
981
993
|
generation.disposed = true
|
|
982
|
-
for (const [, dispose] of
|
|
983
|
-
|
|
994
|
+
for (const [, dispose] of generation.disposers) void dispose()
|
|
995
|
+
generation.disposers.clear()
|
|
984
996
|
}, 'wsl-desktop: variant preset disposal')
|
|
985
997
|
void defaultDistro().then(
|
|
986
998
|
(distro) => refreshPresets(scope, distro, generation),
|
|
Binary file
|
package/lib/wsl/confinement.js
CHANGED
|
@@ -405,12 +405,16 @@ export function buildConfinedCommand({ command, linuxCwd, mode, workspaceLinuxRo
|
|
|
405
405
|
// fence itself and setpriv-exec's the command with NO_NEW_PRIVS — the
|
|
406
406
|
// command text travels as argv after `--` and is never evaluated as root.
|
|
407
407
|
if (runner === RUNNER_HELPER) {
|
|
408
|
+
// Mirrors the direct branch's mode guard: only workspace-write may hand
|
|
409
|
+
// the helper a writable root. workspaceRootInLinux() returns the command's
|
|
410
|
+
// cwd when the policy carries no root, so without this check a read-only
|
|
411
|
+
// command would arrive with a bound, WRITABLE workspace.
|
|
408
412
|
const flags = [
|
|
409
413
|
`--uid ${identity.uid}`,
|
|
410
414
|
`--gid ${identity.gid}`,
|
|
411
415
|
`--home ${shellQuote(identity.home)}`,
|
|
412
416
|
`--cwd ${shellQuote(linuxCwd)}`,
|
|
413
|
-
...(workspaceLinuxRoot !== undefined && workspaceLinuxRoot !== '' ? [`--workspace ${shellQuote(workspaceLinuxRoot)}`] : []),
|
|
417
|
+
...(mode === 'workspace-write' && workspaceLinuxRoot !== undefined && workspaceLinuxRoot !== '' ? [`--workspace ${shellQuote(workspaceLinuxRoot)}`] : []),
|
|
414
418
|
...(isolateProcesses ? [] : ['--no-pidns']),
|
|
415
419
|
'--',
|
|
416
420
|
shellQuote(command),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# dsh-wsl-confine v1 — DSH WSL confinement helper.
|
|
2
|
+
# dsh-wsl-confine v1.1 — DSH WSL confinement helper.
|
|
3
3
|
#
|
|
4
4
|
# Root-owned fence executor: the ONLY thing this helper does is apply the
|
|
5
5
|
# mount-namespace fence (workspace bind, tmpfs /tmp, read-only /, read-only
|
|
@@ -17,9 +17,13 @@
|
|
|
17
17
|
# Contract: parameters are strictly validated (numeric uid/gid, absolute
|
|
18
18
|
# paths); the caller's command travels as argv after `--` and is executed only
|
|
19
19
|
# AFTER the setpriv drop (with NO_NEW_PRIVS) — root never evaluates caller text.
|
|
20
|
+
# The drop identity is checked against the INVOKING user (SUDO_USER): the
|
|
21
|
+
# sudoers grant is argument-wildcarded, and an unchecked --uid would let the
|
|
22
|
+
# session user aim it at uid 0 — the fence is a WRITE boundary, so uid 0 inside
|
|
23
|
+
# it still reads every root-only file. Root's own direct invocation has no
|
|
24
|
+
# SUDO_USER and skips the check.
|
|
20
25
|
set -euo pipefail
|
|
21
|
-
VERSION='dsh-wsl-confine v1'
|
|
22
|
-
SETUP_FAILURE_EXIT=97
|
|
26
|
+
VERSION='dsh-wsl-confine v1.1'
|
|
23
27
|
|
|
24
28
|
uid=; gid=; home=; cwd=; workspace=; pidns=1
|
|
25
29
|
ARGS=()
|
|
@@ -40,7 +44,19 @@ done
|
|
|
40
44
|
[[ "$uid" =~ ^[0-9]+$ && "$gid" =~ ^[0-9]+$ ]] || { echo 'dsh-wsl-confine: uid/gid must be numeric' >&2; exit 2; }
|
|
41
45
|
[[ -n "$cwd" && "$cwd" = /* && -n "$home" && "$home" = /* ]] || { echo 'dsh-wsl-confine: --cwd/--home must be absolute Linux paths' >&2; exit 2; }
|
|
42
46
|
[[ -z "$workspace" || "$workspace" = /* ]] || { echo 'dsh-wsl-confine: --workspace must be absolute' >&2; exit 2; }
|
|
43
|
-
((${#ARGS[@]} >= 1)) || { echo 'dsh-wsl-confine: no command' >&2; exit 2 }
|
|
47
|
+
((${#ARGS[@]} >= 1)) || { echo 'dsh-wsl-confine: no command' >&2; exit 2; }
|
|
48
|
+
|
|
49
|
+
# Identity gate: the caller may only drop to the user sudo says is invoking.
|
|
50
|
+
if [[ -n "${SUDO_USER:-}" ]]; then
|
|
51
|
+
CALLER_RECORD=$(getent passwd "$SUDO_USER" || true)
|
|
52
|
+
CALLER_UID=$(printf '%s' "$CALLER_RECORD" | cut -d: -f3)
|
|
53
|
+
CALLER_GID=$(printf '%s' "$CALLER_RECORD" | cut -d: -f4)
|
|
54
|
+
if [[ -z "$CALLER_UID" || "$uid" != "$CALLER_UID" || "$gid" != "$CALLER_GID" ]]; then
|
|
55
|
+
echo "dsh-wsl-confine: identity mismatch (refusing --uid/--gid for ${SUDO_USER})" >&2
|
|
56
|
+
exit 2
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
44
60
|
command -v unshare >/dev/null 2>&1 || { echo 'dsh-wsl-confine: unshare not found' >&2; exit 97; }
|
|
45
61
|
command -v setpriv >/dev/null 2>&1 || { echo 'dsh-wsl-confine: setpriv not found' >&2; exit 97; }
|
|
46
62
|
command -v findmnt >/dev/null 2>&1 || { echo 'dsh-wsl-confine: findmnt not found' >&2; exit 97; }
|
|
@@ -48,33 +64,38 @@ command -v findmnt >/dev/null 2>&1 || { echo 'dsh-wsl-confine: findmnt not found
|
|
|
48
64
|
# The fence script is built HERE from the validated parameters — the caller
|
|
49
65
|
# never supplies script text. It is identical in effect to the in-process
|
|
50
66
|
# builder (bind workspace before ro, tmpfs /tmp, decoded sweep, postconditions,
|
|
51
|
-
# NO_NEW_PRIVS drop).
|
|
67
|
+
# NO_NEW_PRIVS drop). Parameters cross into `bash -c` as ENVIRONMENT
|
|
68
|
+
# VARIABLES via env(1): bare KEY=VALUE words after the script name would be
|
|
69
|
+
# positional parameters no variable reference can read, and $UID/$GID are
|
|
70
|
+
# bash built-ins that would silently resolve to root's ids under sudo. The
|
|
71
|
+
# exemption pattern anchors ONLY at the group end with an escaped trailing
|
|
72
|
+
# `$` — a literal `/sys$")$"` tail is a bash parse error, not a regex.
|
|
52
73
|
FENCE='set -euo pipefail
|
|
53
74
|
fail() { printf "%s: %s\n" "dsh-wsl-confine" "$1" >&2; exit 97; }
|
|
54
|
-
if [[ -n "${
|
|
55
|
-
mount --bind "$
|
|
56
|
-
KEEP=("/tmp" "$
|
|
75
|
+
if [[ -n "${DROP_WORKSPACE:-}" ]]; then
|
|
76
|
+
mount --bind "$DROP_WORKSPACE" "$DROP_WORKSPACE"
|
|
77
|
+
KEEP=("/tmp" "$DROP_WORKSPACE")
|
|
57
78
|
else
|
|
58
79
|
KEEP=("/tmp")
|
|
59
80
|
fi
|
|
60
81
|
mount -t tmpfs tmpfs /tmp
|
|
61
82
|
mount -o remount,ro,bind /
|
|
62
|
-
findmnt -rno TARGET | while IFS= read -r raw; do printf "%b\n" "$raw"; done | { grep -Ev "^($(printf "%s|" "${KEEP[@]}" | sed "s/|$//")|/dev
|
|
83
|
+
findmnt -rno TARGET | while IFS= read -r raw; do printf "%b\n" "$raw"; done | { grep -Ev "^($(printf "%s|" "${KEEP[@]}" | sed "s/|$//")|/dev|/proc|/sys)\$" || fail "exemption grep failed"; } | while IFS= read -r target; do
|
|
63
84
|
mount -o remount,ro,bind "$target" >/dev/null 2>&1 || true
|
|
64
85
|
done
|
|
65
86
|
mountpoint -q /tmp || fail "/tmp is not a private tmpfs"
|
|
66
87
|
findmnt -rno OPTIONS / | grep -q "^ro" || fail "/ is not read-only"
|
|
67
|
-
findmnt -rno TARGET | while IFS= read -r raw; do printf "%b\n" "$raw"; done | { grep -Ev "^($(printf "%s|" "${KEEP[@]}" | sed "s/|$//")|/dev
|
|
88
|
+
findmnt -rno TARGET | while IFS= read -r raw; do printf "%b\n" "$raw"; done | { grep -Ev "^($(printf "%s|" "${KEEP[@]}" | sed "s/|$//")|/dev|/proc|/sys)\$" || fail "exemption grep failed"; } | while IFS= read -r target; do
|
|
68
89
|
[[ -w "$target" ]] && fail "$target is still writable"
|
|
69
90
|
true
|
|
70
91
|
done
|
|
71
|
-
cd "$
|
|
72
|
-
exec setpriv --no-new-privs --reuid="$
|
|
92
|
+
cd "$DROP_CWD"
|
|
93
|
+
exec setpriv --no-new-privs --reuid="$DROP_UID" --regid="$DROP_GID" --init-groups env HOME="$DROP_HOME" USER="$DROP_USER" LOGNAME="$DROP_USER" bash -lc "$DROP_COMMAND"'
|
|
73
94
|
|
|
74
95
|
PID_FLAG=''
|
|
75
96
|
[[ "$pidns" = 1 ]] && PID_FLAG='--pid --fork'
|
|
76
97
|
|
|
77
|
-
exec unshare --mount --propagation private $PID_FLAG
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
exec unshare --mount --propagation private $PID_FLAG env \
|
|
99
|
+
DROP_UID="$uid" DROP_GID="$gid" DROP_HOME="$home" DROP_USER="${SUDO_USER:-root}" \
|
|
100
|
+
DROP_CWD="$cwd" DROP_WORKSPACE="$workspace" DROP_COMMAND="${ARGS[*]}" \
|
|
101
|
+
bash -c "$FENCE" dsh-wsl-confine
|
package/lib/wsl/preset.js
CHANGED
|
@@ -4,36 +4,23 @@
|
|
|
4
4
|
* A session's execution world is chosen by its preset, not by a global router:
|
|
5
5
|
* the model also needs a different tool dialect (bash rather than PowerShell),
|
|
6
6
|
* and one process-wide provider could not vary that per session. This module
|
|
7
|
-
* therefore rewrites a shipped preset's entry
|
|
8
|
-
* name the host execution world
|
|
9
|
-
*
|
|
7
|
+
* therefore rewrites a shipped preset's plugin entry OBJECTS — it removes the
|
|
8
|
+
* rows that name the host execution world (by canonical id at the top level
|
|
9
|
+
* and by module name at every depth) and appends one `isolate` group that
|
|
10
|
+
* mounts the WSL world plus the tools that consume it.
|
|
10
11
|
*
|
|
11
|
-
* The transform is pure
|
|
12
|
-
* testable without the harness
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* The transform is pure over the loader's entry objects, so it is unit
|
|
13
|
+
* testable without the harness (`scripts/verify-preset.mjs`). The historical
|
|
14
|
+
* 0.1.6 YAML-text renderer lived and died with the disk-generation pipeline:
|
|
15
|
+
* since 0.1.7 variants are registered programmatically via
|
|
16
|
+
* `agentPresets.register({ id, name, plugins })` and the registry, not a
|
|
17
|
+
* generated directory, is the source of truth.
|
|
15
18
|
* @module dsh-wsl-desktop/wsl/preset
|
|
16
19
|
*/
|
|
17
20
|
|
|
18
21
|
import { join } from 'node:path'
|
|
19
22
|
import { pathToFileURL } from 'node:url'
|
|
20
23
|
|
|
21
|
-
/**
|
|
22
|
-
* Decide what happens to one directory inside this plugin's preset namespace.
|
|
23
|
-
*
|
|
24
|
-
* The namespace is a name prefix, so it is shared with anything a user chooses
|
|
25
|
-
* to call `wsl-…`. Only a directory this plugin wrote — one carrying its marker
|
|
26
|
-
* — may be withdrawn; an unmarked one is the user's and is reported instead of
|
|
27
|
-
* deleted.
|
|
28
|
-
* @param {{ name: string, prefix: string, expected: boolean, marked: boolean }} entry - one directory.
|
|
29
|
-
* @returns {'ignore' | 'keep' | 'withdraw' | 'unmanaged'} the disposition.
|
|
30
|
-
*/
|
|
31
|
-
export function sweepDecision({ name, prefix, expected, marked }) {
|
|
32
|
-
if (!name.startsWith(prefix)) return 'ignore'
|
|
33
|
-
if (expected) return 'keep'
|
|
34
|
-
return marked ? 'withdraw' : 'unmanaged'
|
|
35
|
-
}
|
|
36
|
-
|
|
37
24
|
/** Rows that name the host execution world and must not survive in a WSL preset. */
|
|
38
25
|
export const WORLD_ROWS = new Set([
|
|
39
26
|
'tool-bash',
|
|
@@ -113,6 +100,16 @@ export function buildWorldGroup({ subprocessPath, shellPath, fsPath, distro, inc
|
|
|
113
100
|
}
|
|
114
101
|
}
|
|
115
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The sentence appended to the preset's persona.
|
|
105
|
+
*
|
|
106
|
+
* A session's recorded cwd is the UNC spelling (`\\wsl.localhost\<distro>\…`)
|
|
107
|
+
* because that is the only form the Windows-side harness accepts, while the
|
|
108
|
+
* shell in that session reports a Linux path. Without this the model is told one
|
|
109
|
+
* spelling and observes another.
|
|
110
|
+
*/
|
|
111
|
+
export const WSL_PERSONA_SENTENCE = 'Paths under \\\\wsl.localhost\\<distro> are Windows spellings of directories inside that WSL distribution — \\\\wsl.localhost\\<distro>\\home\\me is /home/me there. Every command runs in the distribution, so use Linux paths, and reach Windows files as /mnt/<drive>/…'
|
|
112
|
+
|
|
116
113
|
/**
|
|
117
114
|
* Append the WSL path-dialect sentence to a persona row OBJECT.
|
|
118
115
|
* @param {object} row - the persona entry.
|
|
@@ -130,22 +127,6 @@ function appendPersonaSuffixObject(row) {
|
|
|
130
127
|
return true
|
|
131
128
|
}
|
|
132
129
|
|
|
133
|
-
/**
|
|
134
|
-
* Transform a base preset's plugin ENTRY OBJECTS into the WSL variant's list.
|
|
135
|
-
*
|
|
136
|
-
* Same semantics as the historical YAML rewrite: drop the rows that name the
|
|
137
|
-
* host execution world, amend the persona with the path-dialect sentence,
|
|
138
|
-
* rewrite relative row names against the source directory, and append the
|
|
139
|
-
* `wsl-world` isolate group.
|
|
140
|
-
* @param {readonly object[]} basePlugins - the base preset's plugin rows.
|
|
141
|
-
* @param {object} options - generation options.
|
|
142
|
-
* @param {string} options.subprocessPath - module specifier of the WSL subprocess provider.
|
|
143
|
-
* @param {string} options.shellPath - module specifier of the WSL shell executor.
|
|
144
|
-
* @param {string} options.fsPath - module specifier of the WSL filesystem provider.
|
|
145
|
-
* @param {string | undefined} options.distro - distribution for paths that do not name one.
|
|
146
|
-
* @param {string | undefined} options.sourceDir - directory the source preset lives in.
|
|
147
|
-
* @returns {{ plugins: object[], removed: string[] }} the variant rows and what was dropped.
|
|
148
|
-
*/
|
|
149
130
|
/**
|
|
150
131
|
* Clone one entry row deeply enough to transform it safely. Group rows carry
|
|
151
132
|
* their children under `config` as an ARRAY (recursive tree), so the clone
|
|
@@ -165,6 +146,22 @@ function cloneRow(row) {
|
|
|
165
146
|
return clone
|
|
166
147
|
}
|
|
167
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Transform a base preset's plugin ENTRY OBJECTS into the WSL variant's list.
|
|
151
|
+
*
|
|
152
|
+
* Drop the rows that name the host execution world (id fast path, module-name
|
|
153
|
+
* match at every depth), amend the persona with the path-dialect sentence,
|
|
154
|
+
* rewrite relative row names against the source directory, and append the
|
|
155
|
+
* `wsl-world` isolate group.
|
|
156
|
+
* @param {readonly object[]} basePlugins - the base preset's plugin rows.
|
|
157
|
+
* @param {object} options - generation options.
|
|
158
|
+
* @param {string} options.subprocessPath - module specifier of the WSL subprocess provider.
|
|
159
|
+
* @param {string} options.shellPath - module specifier of the WSL shell executor.
|
|
160
|
+
* @param {string} options.fsPath - module specifier of the WSL filesystem provider.
|
|
161
|
+
* @param {string | undefined} options.distro - distribution for paths that do not name one.
|
|
162
|
+
* @param {string | undefined} options.sourceDir - directory the source preset lives in.
|
|
163
|
+
* @returns {{ plugins: object[], removed: string[] }} the variant rows and what was dropped.
|
|
164
|
+
*/
|
|
168
165
|
export function buildVariantPlugins(basePlugins, { subprocessPath, shellPath, fsPath, distro, sourceDir }) {
|
|
169
166
|
const kept = []
|
|
170
167
|
const removed = []
|
|
@@ -222,235 +219,3 @@ export function buildVariantPlugins(basePlugins, { subprocessPath, shellPath, fs
|
|
|
222
219
|
kept.push(buildWorldGroup({ subprocessPath, shellPath, fsPath, distro, includeEditor }))
|
|
223
220
|
return { plugins: kept, removed }
|
|
224
221
|
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* The sentence appended to the preset's persona.
|
|
228
|
-
*
|
|
229
|
-
* A session's recorded cwd is the UNC spelling (`\\wsl.localhost\<distro>\…`)
|
|
230
|
-
* because that is the only form the Windows-side harness accepts, while the
|
|
231
|
-
* shell in that session reports a Linux path. Without this the model is told one
|
|
232
|
-
* spelling and observes another.
|
|
233
|
-
*/
|
|
234
|
-
export const WSL_PERSONA_SENTENCE = 'Paths under \\\\wsl.localhost\\<distro> are Windows spellings of directories inside that WSL distribution — \\\\wsl.localhost\\<distro>\\home\\me is /home/me there. Every command runs in the distribution, so use Linux paths, and reach Windows files as /mnt/<drive>/…'
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Append the WSL sentence to a persona row's text field.
|
|
238
|
-
*
|
|
239
|
-
* Handles the three shapes a preset may use: an inline `suffix`, a block-scalar
|
|
240
|
-
* `suffix`, and a persona that carries only `text`. A row with none of them
|
|
241
|
-
* gains a `suffix` under `config:`.
|
|
242
|
-
* @param {{ id: string | null, lines: string[] }} block - the persona block.
|
|
243
|
-
* @returns {boolean} whether the block was amended.
|
|
244
|
-
*/
|
|
245
|
-
export function appendPersonaSuffix(block) {
|
|
246
|
-
const lines = block.lines
|
|
247
|
-
const configIndex = lines.findIndex((line) => /^(\s*)config:\s*$/.test(line))
|
|
248
|
-
if (configIndex === -1) return false
|
|
249
|
-
const configIndent = (/^(\s*)config:\s*$/.exec(lines[configIndex])?.[1] ?? '').length
|
|
250
|
-
const childIndent = configIndent + 2
|
|
251
|
-
const child = new RegExp(`^ {${childIndent}}(suffix|text|prefix):(.*)$`)
|
|
252
|
-
for (const field of ['suffix', 'text', 'prefix']) {
|
|
253
|
-
const index = lines.findIndex((line, at) => at > configIndex && child.test(line) && new RegExp(`^ {${childIndent}}${field}:`).test(line))
|
|
254
|
-
if (index === -1) continue
|
|
255
|
-
const value = (new RegExp(`^ {${childIndent}}${field}:(.*)$`).exec(lines[index])?.[1] ?? '').trim()
|
|
256
|
-
// A block scalar (`>-`, `|`, `|-`, …) continues on the following lines.
|
|
257
|
-
if (value === '' || /^[|>][-+]?\d*$/.test(value)) {
|
|
258
|
-
lines.splice(index + 1, 0, `${' '.repeat(childIndent + 2)}${WSL_PERSONA_SENTENCE}`)
|
|
259
|
-
} else if ((value.startsWith("'") && value.endsWith("'") && value.length >= 2)
|
|
260
|
-
|| (value.startsWith('"') && value.endsWith('"') && value.length >= 2)) {
|
|
261
|
-
// A quoted scalar must grow INSIDE its quotes; appending after a closing
|
|
262
|
-
// quote would produce invalid YAML.
|
|
263
|
-
const quote = value[0]
|
|
264
|
-
const sentence = quote === "'" ? WSL_PERSONA_SENTENCE.replace(/'/g, "''") : WSL_PERSONA_SENTENCE
|
|
265
|
-
lines[index] = `${lines[index].slice(0, -1)} ${sentence}${quote}`
|
|
266
|
-
} else {
|
|
267
|
-
lines[index] = `${lines[index]} ${WSL_PERSONA_SENTENCE}`
|
|
268
|
-
}
|
|
269
|
-
return true
|
|
270
|
-
}
|
|
271
|
-
// No text field of its own: gain one as the first key under `config:`.
|
|
272
|
-
lines.splice(configIndex + 1, 0, `${' '.repeat(childIndent)}suffix: ${WSL_PERSONA_SENTENCE}`)
|
|
273
|
-
return true
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* Quote one scalar for a YAML single-quoted value.
|
|
278
|
-
* @param {string} value - the raw value.
|
|
279
|
-
* @returns {string} the quoted scalar.
|
|
280
|
-
*/
|
|
281
|
-
function quote(value) {
|
|
282
|
-
return `'${value.replace(/'/g, "''")}'`
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Split an entry list into top-level blocks.
|
|
287
|
-
* @param {string} source - the loader entry list.
|
|
288
|
-
* @returns {Array<{ id: string | null, lines: string[] }>} the blocks in order.
|
|
289
|
-
*/
|
|
290
|
-
export function splitBlocks(source) {
|
|
291
|
-
const blocks = []
|
|
292
|
-
let current = { id: null, lines: [] }
|
|
293
|
-
for (const line of source.split('\n')) {
|
|
294
|
-
const match = /^- id:\s*(.+?)\s*$/.exec(line)
|
|
295
|
-
if (match !== null) {
|
|
296
|
-
if (current.lines.length > 0 || current.id !== null) blocks.push(current)
|
|
297
|
-
// Normalise the captured id so every YAML spelling of the same row
|
|
298
|
-
// resolves to one canonical id for world-row removal: strip trailing
|
|
299
|
-
// comments, YAML anchors (`&a name`), and surrounding quotes.
|
|
300
|
-
let id = match[1].replace(/\s+#.*$/, '').trim()
|
|
301
|
-
if (id.startsWith('&')) id = id.replace(/^&\S+\s+/, '')
|
|
302
|
-
id = id.replace(/^"(.*)"$/, '$1').replace(/^'(.*)'$/, '$1')
|
|
303
|
-
current = { id, lines: [line] }
|
|
304
|
-
continue
|
|
305
|
-
}
|
|
306
|
-
current.lines.push(line)
|
|
307
|
-
}
|
|
308
|
-
blocks.push(current)
|
|
309
|
-
return blocks
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* Build the `wsl-world` group that carries the WSL execution world.
|
|
314
|
-
*
|
|
315
|
-
* The group isolates every service a row inside it publishes, so one session
|
|
316
|
-
* can run in a distribution while the process keeps serving Windows sessions.
|
|
317
|
-
* `subprocess-wsl` precedes the consumers that inject it: the shell executor
|
|
318
|
-
* hands it a Linux argv and lets the provider own the `wsl.exe` wrapper.
|
|
319
|
-
* @param {object} options - generation options.
|
|
320
|
-
* @param {string} options.subprocessPath - module specifier of the WSL subprocess provider.
|
|
321
|
-
* @param {string} options.shellPath - module specifier of the WSL shell executor.
|
|
322
|
-
* @param {string} options.fsPath - module specifier of the WSL filesystem provider.
|
|
323
|
-
* @param {string | undefined} options.distro - distribution for paths that do not name one.
|
|
324
|
-
* @param {boolean} options.includeEditor - whether the source preset mounted the string-replace editor.
|
|
325
|
-
* @returns {string[]} the group's YAML lines.
|
|
326
|
-
*/
|
|
327
|
-
function worldGroup({ subprocessPath, shellPath, fsPath, distro, includeEditor }) {
|
|
328
|
-
const distroConfig = distro === undefined ? [] : [' config:', ` distro: ${quote(distro)}`]
|
|
329
|
-
return [
|
|
330
|
-
'- id: wsl-world',
|
|
331
|
-
' name: cordis:group',
|
|
332
|
-
' group: true',
|
|
333
|
-
' isolate:',
|
|
334
|
-
' shell: true',
|
|
335
|
-
' fs: true',
|
|
336
|
-
' subprocess: true',
|
|
337
|
-
' config:',
|
|
338
|
-
' - id: subprocess-wsl',
|
|
339
|
-
` name: ${quote(subprocessPath)}`,
|
|
340
|
-
...distroConfig,
|
|
341
|
-
' - id: shell-wsl',
|
|
342
|
-
` name: ${quote(shellPath)}`,
|
|
343
|
-
...distroConfig,
|
|
344
|
-
' - id: fs-wsl',
|
|
345
|
-
` name: ${quote(fsPath)}`,
|
|
346
|
-
...distroConfig,
|
|
347
|
-
// No terminal registry row: the Web terminal controller spawns through
|
|
348
|
-
// `agent.ctx.get('subprocess').spawnTerminal(...)`
|
|
349
|
-
// (packages/api/terminal-controller/src/index.ts:333,346), so the realm's
|
|
350
|
-
// subprocess provider already puts the PTY inside the distribution. A
|
|
351
|
-
// `terminals` realm would only serve the model's persistent-shell tool,
|
|
352
|
-
// which this preset does not mount.
|
|
353
|
-
' - id: tool-bash',
|
|
354
|
-
" name: '@deepseek-ai/dsh-tool-bash'",
|
|
355
|
-
' - id: tool-fs',
|
|
356
|
-
" name: '@deepseek-ai/dsh-tool-fs'",
|
|
357
|
-
...includeEditor
|
|
358
|
-
? [' - id: str-replace-editor', " name: '@deepseek-ai/dsh-tool-str-replace-editor'"]
|
|
359
|
-
: [],
|
|
360
|
-
]
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* Rewrite relative row names so they still resolve from the variant directory.
|
|
365
|
-
*
|
|
366
|
-
* A preset may name a row by a path relative to its own directory
|
|
367
|
-
* (`./tool-bootstrap.mjs`). The variant lives in a different directory, so the
|
|
368
|
-
* relative spelling would resolve against the wrong base and the preset would
|
|
369
|
-
* fail to mount.
|
|
370
|
-
* @param {{ lines: string[] }} block - one top-level entry.
|
|
371
|
-
* @param {string} sourceDir - directory of the preset being derived from.
|
|
372
|
-
* @returns {number} how many names were rewritten.
|
|
373
|
-
*/
|
|
374
|
-
export function rewriteRelativeNames(block, sourceDir) {
|
|
375
|
-
let rewritten = 0
|
|
376
|
-
block.lines = block.lines.map((line) => {
|
|
377
|
-
const match = /^(\s*)name:\s*['"]?\.\/([^'"]+)['"]?\s*$/.exec(line)
|
|
378
|
-
if (match === null) return line
|
|
379
|
-
const absolute = join(sourceDir, match[2]).replace(/\\/g, '/')
|
|
380
|
-
rewritten += 1
|
|
381
|
-
return `${match[1]}name: '${absolute}'`
|
|
382
|
-
})
|
|
383
|
-
return rewritten
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Rewrite one shipped preset into its WSL variant.
|
|
388
|
-
*
|
|
389
|
-
* The whole preset is wrapped in the realm rather than sitting beside it. A
|
|
390
|
-
* sibling group would leave every consumer outside it — a persistent-shell
|
|
391
|
-
* group, a hand-written tool row — resolving the host's providers, which is the
|
|
392
|
-
* Windows execution world; wrapping makes the realm the preset's own scope, so
|
|
393
|
-
* every row resolves the WSL providers. Rows that hardcode Windows tooling are
|
|
394
|
-
* dropped and re-mounted in their WSL form.
|
|
395
|
-
* @param {string} source - the shipped preset's `agent.cordis.yml` text.
|
|
396
|
-
* @param {object} options - generation options.
|
|
397
|
-
* @param {string} options.subprocessPath - module specifier of the WSL subprocess provider.
|
|
398
|
-
* @param {string} options.shellPath - module specifier of the WSL shell executor.
|
|
399
|
-
* @param {string} options.fsPath - module specifier of the WSL filesystem provider.
|
|
400
|
-
* @param {string} [options.distro] - distribution for paths that do not name one.
|
|
401
|
-
* @param {string} [options.sourceDir] - directory the source preset lives in.
|
|
402
|
-
* @returns {{ yaml: string, removed: string[], added: boolean }} the rewritten preset and what changed.
|
|
403
|
-
*/
|
|
404
|
-
export function renderWslPreset(source, options) {
|
|
405
|
-
const blocks = splitBlocks(source)
|
|
406
|
-
const kept = []
|
|
407
|
-
const removed = []
|
|
408
|
-
let includeEditor = false
|
|
409
|
-
let personaAmended = false
|
|
410
|
-
for (const block of blocks) {
|
|
411
|
-
if (block.id !== null && WORLD_ROWS.has(block.id)) {
|
|
412
|
-
removed.push(block.id)
|
|
413
|
-
if (block.id === 'str-replace-editor') includeEditor = true
|
|
414
|
-
continue
|
|
415
|
-
}
|
|
416
|
-
if (block.id === 'persona') personaAmended = appendPersonaSuffix(block)
|
|
417
|
-
if (options.sourceDir !== undefined) rewriteRelativeNames(block, options.sourceDir)
|
|
418
|
-
kept.push(block)
|
|
419
|
-
}
|
|
420
|
-
// `split('\n')` consumed the separators, so blocks rejoin with one newline;
|
|
421
|
-
// joining with '' would glue the next `- id:` onto the previous line.
|
|
422
|
-
const body = kept.map((block) => block.lines.join('\n')).join('\n')
|
|
423
|
-
// One level down: the original entry list becomes the group's `config` list.
|
|
424
|
-
const nested = body
|
|
425
|
-
.split('\n')
|
|
426
|
-
.map((line) => (line.trim() === '' ? line : ` ${line}`))
|
|
427
|
-
.join('\n')
|
|
428
|
-
const group = worldGroup({
|
|
429
|
-
subprocessPath: options.subprocessPath,
|
|
430
|
-
shellPath: options.shellPath,
|
|
431
|
-
fsPath: options.fsPath,
|
|
432
|
-
distro: options.distro,
|
|
433
|
-
includeEditor,
|
|
434
|
-
})
|
|
435
|
-
return {
|
|
436
|
-
yaml: `${group.join('\n')}\n${nested}\n`,
|
|
437
|
-
removed,
|
|
438
|
-
added: true,
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Render the preset's display metadata.
|
|
444
|
-
* @param {{ name: string, description: string }} metadata - display name and description.
|
|
445
|
-
* @returns {string} the `preset.yml` text.
|
|
446
|
-
*/
|
|
447
|
-
export function renderPresetMetadata(metadata) {
|
|
448
|
-
// Quote only when the value contains characters that YAML would
|
|
449
|
-
// misinterpret; plain alphanumeric + CJK + basic punctuation stays unquoted.
|
|
450
|
-
const scalar = (value) => {
|
|
451
|
-
const s = String(value)
|
|
452
|
-
if (/^[\w.\-\u4e00-\u9fff\u3000-\u303f\u00b7(): ]+$/u.test(s) && !/^\s|\s$/.test(s) && !s.includes(': ') && !s.startsWith('#')) return s
|
|
453
|
-
return `'${s.replace(/'/g, "''")}'`
|
|
454
|
-
}
|
|
455
|
-
return `name: ${scalar(metadata.name)}\ndescription: ${scalar(metadata.description)}\n`
|
|
456
|
-
}
|
package/lib/wsl/shell.js
CHANGED
|
@@ -344,7 +344,12 @@ export class WslShellExecutor extends ShellExecutor {
|
|
|
344
344
|
sandbox: { ...sandbox },
|
|
345
345
|
readOutput() {
|
|
346
346
|
const stdoutRead = stdout.readFrom(readOffset)
|
|
347
|
-
|
|
347
|
+
// Resume from the reader's own nextOffset, not `text.length`: the
|
|
348
|
+
// collect contract is BYTE-offset based (SubprocessOutputRead), and
|
|
349
|
+
// a UTF-16 code-unit count diverges from it for any non-ASCII
|
|
350
|
+
// output — the second read would resume mid-stream and duplicate or
|
|
351
|
+
// garble text. start() already advanced this way.
|
|
352
|
+
readOffset = stdoutRead.nextOffset
|
|
348
353
|
const stderrRead = stderr.readFrom(0)
|
|
349
354
|
return {
|
|
350
355
|
delta: stderrRead.text !== '' ? `${stdoutRead.text}\n[stderr]\n${stderrRead.text}` : stdoutRead.text,
|
|
@@ -159,8 +159,14 @@ class Bridge:
|
|
|
159
159
|
|
|
160
160
|
op = request.get("op")
|
|
161
161
|
if op == "resize":
|
|
162
|
-
|
|
163
|
-
|
|
162
|
+
try:
|
|
163
|
+
self.resize(request.get("cols", 80), request.get("rows", 24))
|
|
164
|
+
ans({"ok": True})
|
|
165
|
+
except (TypeError, ValueError, OSError) as error:
|
|
166
|
+
# A malformed payload must not kill the session: the bridge IS
|
|
167
|
+
# the terminal, so an unhandled exception in this dispatch
|
|
168
|
+
# would take down every later control op and the PTY itself.
|
|
169
|
+
ans({"ok": False, "error": str(error)})
|
|
164
170
|
elif op == "foreground":
|
|
165
171
|
pgrp = self.foreground_pgrp()
|
|
166
172
|
ans({"ok": True, "pgrp": pgrp, "shellPgrp": self.shell_pgrp})
|
|
@@ -196,7 +202,11 @@ class Bridge:
|
|
|
196
202
|
break
|
|
197
203
|
deadline = time.monotonic() + wait
|
|
198
204
|
while time.monotonic() < deadline:
|
|
199
|
-
|
|
205
|
+
try:
|
|
206
|
+
done, _ = os.waitpid(self.pid, os.WNOHANG)
|
|
207
|
+
except ChildProcessError:
|
|
208
|
+
# The pump loop already reaped the child: session gone.
|
|
209
|
+
return True
|
|
200
210
|
if done == self.pid:
|
|
201
211
|
return True
|
|
202
212
|
time.sleep(0.05)
|
package/lib/wsl/world.js
CHANGED
|
@@ -176,16 +176,11 @@ export function hostExecutable(name) {
|
|
|
176
176
|
* @returns {NodeJS.ProcessEnv} the environment for the `wsl.exe` process.
|
|
177
177
|
*/
|
|
178
178
|
function bridgeEnv(extra) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
const ambient = process.env.WSLENV
|
|
186
|
-
const merged = [ambient, flags.join(':')].filter((part) => part !== undefined && part !== '').join(':')
|
|
187
|
-
if (merged !== '') env.WSLENV = merged
|
|
188
|
-
return env
|
|
179
|
+
// Flag-building is shared with withWslEnvFlags so the WSLENV metacharacter
|
|
180
|
+
// filter cannot drift apart between the two call sites again (bridgeEnv
|
|
181
|
+
// used to accept `:`/`/`-bearing keys that withWslEnvFlags rejects). env is
|
|
182
|
+
// optional — the common probe path passes none.
|
|
183
|
+
return { ...process.env, ...ENV_OVERRIDES, ...withWslEnvFlags(extra ?? {}) }
|
|
189
184
|
}
|
|
190
185
|
|
|
191
186
|
/**
|
|
@@ -404,7 +399,7 @@ export async function resolveDistroHome(distro, username) {
|
|
|
404
399
|
let user = typeof username === 'string' ? username.trim() : ''
|
|
405
400
|
if (user === '') {
|
|
406
401
|
// Non-login + trimmed on purpose: the parsed answer must be provably this
|
|
407
|
-
// probe's output (
|
|
402
|
+
// probe's output (the login name), never profile scripts'.
|
|
408
403
|
const who = await runWslShell({ distro, linuxCwd: '/', command: 'id -un', loginShell: false, timeoutMs: 30_000 })
|
|
409
404
|
user = who.stdout.trim()
|
|
410
405
|
if (user === '' || user.includes('\n')) {
|
package/package.json
CHANGED
|
@@ -1,35 +1,37 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-wsl-desktop",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"main": "lib/index.js",
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"
|
|
35
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-wsl-desktop",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.19"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"lib",
|
|
11
|
+
"cordis.patch.yml",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"dsh": {
|
|
16
|
+
"bundle": {
|
|
17
|
+
"patch": "./cordis.patch.yml"
|
|
18
|
+
},
|
|
19
|
+
"client": {
|
|
20
|
+
"platform": "web",
|
|
21
|
+
"inject": [
|
|
22
|
+
"@deepseek-ai/dsh-api-workspace-controller",
|
|
23
|
+
"@deepseek-ai/dsh-client-ui-workspace"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/zcluo/dsh-wsl-desktop.git"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"manifestVersion": 1,
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"description": "DSH Desktop WSL execution world: add a WSL workspace and run every tool inside the distribution."
|
|
37
|
+
}
|