@sparkelf/dsh-patch-wsl-native-open 0.2.0-rc.5
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/LICENSE +21 -0
- package/package.json +36 -0
- package/patches/wsl-native-open.patch +259 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Data-only exact official-source patch translating POSIX paths for the Windows interpreter on a WSL host",
|
|
3
|
+
"dshPatch": {
|
|
4
|
+
"formatVersion": 1,
|
|
5
|
+
"variants": [
|
|
6
|
+
{
|
|
7
|
+
"dsh": ">=0.1.6-alpha.1",
|
|
8
|
+
"file": "./patches/wsl-native-open.patch",
|
|
9
|
+
"id": "wsl-native-path-translation",
|
|
10
|
+
"target": {
|
|
11
|
+
"baseRevision": "0a15e36e7f82b6ed45af6fa9759f29b40dcd965d",
|
|
12
|
+
"kind": "dsh-source",
|
|
13
|
+
"paths": [
|
|
14
|
+
"packages/host/open-in-app/",
|
|
15
|
+
"packages/util/native-command/"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"patches/*.patch"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"name": "@sparkelf/dsh-patch-wsl-native-open",
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
},
|
|
29
|
+
"repository": {
|
|
30
|
+
"directory": "patches/npm/wsl-native-open",
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/SparkElf/deepseek-harness-plus.git"
|
|
33
|
+
},
|
|
34
|
+
"type": "module",
|
|
35
|
+
"version": "0.2.0-rc.5"
|
|
36
|
+
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
diff --git a/packages/host/open-in-app/src/icons.ts b/packages/host/open-in-app/src/icons.ts
|
|
2
|
+
index 6908ce7..570dc45 100644
|
|
3
|
+
--- a/packages/host/open-in-app/src/icons.ts
|
|
4
|
+
+++ b/packages/host/open-in-app/src/icons.ts
|
|
5
|
+
@@ -97,6 +97,41 @@ const EXTRACT_ICON_PS1 = [
|
|
6
|
+
'',
|
|
7
|
+
].join('\n')
|
|
8
|
+
|
|
9
|
+
+/**
|
|
10
|
+
+ * Translate POSIX paths for the Windows interpreter when this host is WSL.
|
|
11
|
+
+ *
|
|
12
|
+
+ * `Convert-Path` and `System.Drawing` reject a Linux path: the extension host maps
|
|
13
|
+
+ * `/mnt/c/...` itself, but PowerShell running as a Windows process resolves
|
|
14
|
+
+ * `\\wsl.localhost\...` and refuses to treat it as a file. `wslpath -w` produces the
|
|
15
|
+
+ * form it accepts. A non-WSL host returns its paths unchanged.
|
|
16
|
+
+ *
|
|
17
|
+
+ * @param paths - absolute POSIX paths to hand to the interpreter.
|
|
18
|
+
+ * @param timeoutMs - per-command deadline for the translation.
|
|
19
|
+
+ * @param internals - platform and runner hooks for deterministic tests.
|
|
20
|
+
+ * @returns the translated paths, or null when a translation failed.
|
|
21
|
+
+ */
|
|
22
|
+
+async function windowsPaths(
|
|
23
|
+
+ paths: readonly string[], timeoutMs: number, internals: ResolvedInternals,
|
|
24
|
+
+): Promise<string[] | null> {
|
|
25
|
+
+ if (!isWslHost(internals)) return [...paths]
|
|
26
|
+
+ const translated: string[] = []
|
|
27
|
+
+ for (const path of paths) {
|
|
28
|
+
+ const windows = await output('wslpath', ['-w', path], timeoutMs, internals)
|
|
29
|
+
+ const trimmed = windows?.replace(/[\r\n]+$/u, '') ?? ''
|
|
30
|
+
+ if (trimmed === '') return null
|
|
31
|
+
+ translated.push(trimmed)
|
|
32
|
+
+ }
|
|
33
|
+
+ return translated
|
|
34
|
+
+}
|
|
35
|
+
+
|
|
36
|
+
+/** Whether this Host is a WSL distribution, whose Windows desktop takes the path. */
|
|
37
|
+
+function isWslHost(internals: ResolvedInternals): boolean {
|
|
38
|
+
+ if (internals.platform !== 'linux') return false
|
|
39
|
+
+ const env = internals.env
|
|
40
|
+
+ if ((env.WSL_DISTRO_NAME ?? '') !== '' || (env.WSL_INTEROP ?? '') !== '') return true
|
|
41
|
+
+ return internals.osRelease.toLowerCase().includes('microsoft')
|
|
42
|
+
+}
|
|
43
|
+
+
|
|
44
|
+
/** Extract one Windows executable's associated icon as a 32px PNG. */
|
|
45
|
+
async function extractExecutableIconPng(
|
|
46
|
+
executablePath: string, timeoutMs: number, internals: ResolvedInternals,
|
|
47
|
+
@@ -106,8 +141,12 @@ async function extractExecutableIconPng(
|
|
48
|
+
const script = join(workDir, 'extract-icon.ps1')
|
|
49
|
+
const outPng = join(workDir, 'icon.png')
|
|
50
|
+
await writeFile(script, EXTRACT_ICON_PS1, 'utf8')
|
|
51
|
+
+ // Windows PowerShell reads Windows paths, so a WSL host translates all three before
|
|
52
|
+
+ // handing them over; the unmounted caller passes them through unchanged.
|
|
53
|
+
+ const args = await windowsPaths([script, executablePath, outPng], timeoutMs, internals)
|
|
54
|
+
+ if (args === null) return null
|
|
55
|
+
const ran = await output('powershell.exe', [
|
|
56
|
+
- '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script, executablePath, outPng,
|
|
57
|
+
+ '-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', ...args,
|
|
58
|
+
], timeoutMs, internals)
|
|
59
|
+
if (ran === null) return null
|
|
60
|
+
try {
|
|
61
|
+
@@ -192,6 +231,13 @@ export async function extractAppIcon(
|
|
62
|
+
): Promise<OpenInAppIcon | null> {
|
|
63
|
+
const completed = resolveInternals(internals)
|
|
64
|
+
if (completed.platform === 'linux') {
|
|
65
|
+
+ // A resolved executable icon takes precedence on Linux: WSL reaches the Windows
|
|
66
|
+
+ // shell open and its Explorer icon lives in the executable, while the desktop-entry
|
|
67
|
+
+ // lookup below has nothing to read on a host with no Linux desktop at all.
|
|
68
|
+
+ if (resolved.icon?.kind === 'executable') {
|
|
69
|
+
+ const bytes = await extractExecutableIconPng(resolved.icon.path, timeoutMs, completed)
|
|
70
|
+
+ return bytes === null ? null : { bytes, contentType: 'image/png' }
|
|
71
|
+
+ }
|
|
72
|
+
const desktopId = specFor(app, completed.platform)?.desktopId
|
|
73
|
+
return desktopId === undefined ? null : extractLinuxIcon(desktopId, completed)
|
|
74
|
+
}
|
|
75
|
+
diff --git a/packages/host/open-in-app/src/resolver.ts b/packages/host/open-in-app/src/resolver.ts
|
|
76
|
+
index 7ab964b..8ea7318 100644
|
|
77
|
+
--- a/packages/host/open-in-app/src/resolver.ts
|
|
78
|
+
+++ b/packages/host/open-in-app/src/resolver.ts
|
|
79
|
+
@@ -14,10 +14,10 @@
|
|
80
|
+
|
|
81
|
+
import { spawn } from 'node:child_process'
|
|
82
|
+
import { readdir, readFile, stat } from 'node:fs/promises'
|
|
83
|
+
-import { homedir, platform as osPlatform } from 'node:os'
|
|
84
|
+
+import { homedir, release as osRelease, platform as osPlatform } from 'node:os'
|
|
85
|
+
import { dirname, isAbsolute, join } from 'node:path'
|
|
86
|
+
import {
|
|
87
|
+
- canOpenNativePath, openNativePath, runNativeCommand, type NativeCommandRunner,
|
|
88
|
+
+ canOpenLinuxDesktop, canOpenNativePath, openNativePath, runNativeCommand, type NativeCommandRunner,
|
|
89
|
+
} from '@deepseek-ai/dsh-native-command'
|
|
90
|
+
import { scrubbedParentEnv } from '@deepseek-ai/dsh-subprocess'
|
|
91
|
+
import {
|
|
92
|
+
@@ -98,6 +98,8 @@ export const launchDetachedApp: OpenInAppLauncher = (command, args, options) =>
|
|
93
|
+
/** Injectable platform facts for deterministic tests. */
|
|
94
|
+
export interface OpenInAppInternals {
|
|
95
|
+
platform?: NodeJS.Platform
|
|
96
|
+
+ /** Kernel release override; distinguishes WSL from desktop Linux without a probe. */
|
|
97
|
+
+ osRelease?: string
|
|
98
|
+
/** SSH launch fact from the inherited process layer, independent of `.env` values. */
|
|
99
|
+
ssh?: boolean
|
|
100
|
+
/** Bundle-directory roots replacing `/Applications` and `~/Applications`. */
|
|
101
|
+
@@ -115,6 +117,7 @@ export interface OpenInAppInternals {
|
|
102
|
+
/** Platform facts after the one explicit defaulting step at each public entry. */
|
|
103
|
+
export interface ResolvedInternals {
|
|
104
|
+
platform: NodeJS.Platform
|
|
105
|
+
+ osRelease: string
|
|
106
|
+
ssh: boolean
|
|
107
|
+
applicationRoots: readonly string[]
|
|
108
|
+
env: Readonly<Record<string, string | undefined>>
|
|
109
|
+
@@ -140,6 +143,7 @@ export function resolveInternals(internals: OpenInAppInternals): ResolvedInterna
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
platform: internals.platform ?? osPlatform(),
|
|
113
|
+
+ osRelease: internals.osRelease ?? osRelease(),
|
|
114
|
+
ssh: internals.ssh ?? false,
|
|
115
|
+
applicationRoots: internals.applicationRoots ?? ['/Applications', join(home, 'Applications')],
|
|
116
|
+
env: internals.env ?? process.env,
|
|
117
|
+
@@ -467,6 +471,14 @@ export function specFor(app: OpenInAppApp, platform: NodeJS.Platform): OpenInApp
|
|
118
|
+
: undefined
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
+/**
|
|
122
|
+
+ * Explorer icon for the Windows shell open a WSL host resolves to.
|
|
123
|
+
+ *
|
|
124
|
+
+ * \`SystemRoot\` is unset inside WSL, so the mounted drive is named directly; the
|
|
125
|
+
+ * command-line \`openWindowsPath\` reaches the same interpreter through the same mount.
|
|
126
|
+
+ */
|
|
127
|
+
+const WINDOWS_EXPLORER_ICON = '/mnt/c/Windows/explorer.exe'
|
|
128
|
+
+
|
|
129
|
+
/** Icon source for a resolved executable: Windows extracts from the binary itself. */
|
|
130
|
+
function executableIcon(path: string, internals: ResolvedInternals): OpenInAppIconSource | undefined {
|
|
131
|
+
return internals.platform === 'win32' ? { kind: 'executable', path } : undefined
|
|
132
|
+
@@ -518,10 +530,29 @@ async function locate(
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
case 'cli': {
|
|
136
|
+
- if (locator.requiresDesktop === true && !canOpenNativePath({
|
|
137
|
+
- platform: internals.platform,
|
|
138
|
+
- env: { ...internals.env },
|
|
139
|
+
- })) return null
|
|
140
|
+
+ // A desktop CLI locator names a program that draws on the host's own Linux
|
|
141
|
+
+ // desktop. WSL has no such program, but it does have the Windows desktop, so
|
|
142
|
+
+ // the entry stays and resolves to the Windows shell open instead of a launcher
|
|
143
|
+
+ // this host cannot run.
|
|
144
|
+
+ if (locator.requiresDesktop === true) {
|
|
145
|
+
+ const facts = {
|
|
146
|
+
+ platform: internals.platform,
|
|
147
|
+
+ osRelease: internals.osRelease,
|
|
148
|
+
+ env: { ...internals.env },
|
|
149
|
+
+ }
|
|
150
|
+
+ const linuxDesktop = canOpenLinuxDesktop(facts)
|
|
151
|
+
+ // WSL answers no here yet can still open a path, because the Windows desktop
|
|
152
|
+
+ // takes it. That combination is what selects the shell open below; a headless
|
|
153
|
+
+ // Linux host answers no to both and keeps dropping the entry.
|
|
154
|
+
+ if (!linuxDesktop && canOpenNativePath(facts)) {
|
|
155
|
+
+ const iconPath = expandCandidate(WINDOWS_EXPLORER_ICON, internals)
|
|
156
|
+
+ return {
|
|
157
|
+
+ launch: { kind: 'shell-open' },
|
|
158
|
+
+ icon: iconPath === null ? undefined : { kind: 'executable', path: iconPath },
|
|
159
|
+
+ }
|
|
160
|
+
+ }
|
|
161
|
+
+ if (!linuxDesktop) return null
|
|
162
|
+
+ }
|
|
163
|
+
const found = await internals.resolveExecutable(locator.name)
|
|
164
|
+
return found === null
|
|
165
|
+
? null
|
|
166
|
+
diff --git a/packages/util/native-command/src/index.ts b/packages/util/native-command/src/index.ts
|
|
167
|
+
index a2e23a3..f1ea7c6 100644
|
|
168
|
+
--- a/packages/util/native-command/src/index.ts
|
|
169
|
+
+++ b/packages/util/native-command/src/index.ts
|
|
170
|
+
@@ -6,6 +6,7 @@
|
|
171
|
+
export { runNativeCommand } from './runner.ts'
|
|
172
|
+
export type { NativeCommandRunner } from './runner.ts'
|
|
173
|
+
export {
|
|
174
|
+
+ canOpenLinuxDesktop,
|
|
175
|
+
canOpenNativePath,
|
|
176
|
+
nativeFileManager,
|
|
177
|
+
revealNativePath,
|
|
178
|
+
diff --git a/packages/util/native-command/src/path-opener.ts b/packages/util/native-command/src/path-opener.ts
|
|
179
|
+
index 8f3443b..79e7ace 100644
|
|
180
|
+
--- a/packages/util/native-command/src/path-opener.ts
|
|
181
|
+
+++ b/packages/util/native-command/src/path-opener.ts
|
|
182
|
+
@@ -99,13 +99,49 @@ function isWsl(internals: PathOpenerInternals): boolean {
|
|
183
|
+
return (internals.osRelease ?? osRelease()).toLowerCase().includes('microsoft')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
+/**
|
|
187
|
+
+ * Windows PowerShell locations to try when PATH carries no Windows entry.
|
|
188
|
+
+ *
|
|
189
|
+
+ * WSL injects its Windows directories into PATH only for the processes it starts
|
|
190
|
+
+ * interactively, so a service, a scheduled job, or any process started after that
|
|
191
|
+
+ * injection was lost resolves nothing by name. The interpreter is still present on
|
|
192
|
+
+ * the volume the distribution mounts, and naming it there is what keeps the Windows
|
|
193
|
+
+ * desktop reachable from those processes.
|
|
194
|
+
+ */
|
|
195
|
+
+const WINDOWS_POWERSHELL_CANDIDATES = [
|
|
196
|
+
+ '/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',
|
|
197
|
+
+ '/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe',
|
|
198
|
+
+] as const
|
|
199
|
+
+
|
|
200
|
+
/** Open one Windows-resolvable path through its registered desktop application. */
|
|
201
|
+
async function openWindowsPath(path: string, signal: AbortSignal, run: PathOpenerRunner): Promise<void> {
|
|
202
|
+
- await run('powershell.exe', [
|
|
203
|
+
+ const args = [
|
|
204
|
+
'-NoProfile',
|
|
205
|
+
'-Command',
|
|
206
|
+
`Invoke-Item -LiteralPath ${powershellLiteral(path)}`,
|
|
207
|
+
- ], signal)
|
|
208
|
+
+ ]
|
|
209
|
+
+ try {
|
|
210
|
+
+ await run('powershell.exe', args, signal)
|
|
211
|
+
+ return
|
|
212
|
+
+ } catch (error) {
|
|
213
|
+
+ // A missing executable is the one failure a mounted path can answer; a non-zero
|
|
214
|
+
+ // exit from PowerShell itself is a real launch failure and must stay reported.
|
|
215
|
+
+ if (!isMissingExecutable(error)) throw error
|
|
216
|
+
+ }
|
|
217
|
+
+ for (const candidate of WINDOWS_POWERSHELL_CANDIDATES) {
|
|
218
|
+
+ try {
|
|
219
|
+
+ await run(candidate, args, signal)
|
|
220
|
+
+ return
|
|
221
|
+
+ } catch (error) {
|
|
222
|
+
+ if (!isMissingExecutable(error)) throw error
|
|
223
|
+
+ }
|
|
224
|
+
+ }
|
|
225
|
+
+ throw new Error('Windows PowerShell is not reachable from this WSL mount')
|
|
226
|
+
+}
|
|
227
|
+
+
|
|
228
|
+
+/** Whether a spawn failed because the executable could not be found at all. */
|
|
229
|
+
+function isMissingExecutable(error: unknown): boolean {
|
|
230
|
+
+ return (error as { code?: unknown } | null | undefined)?.code === 'ENOENT'
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Translate a WSL path before handing it to the Windows desktop. */
|
|
234
|
+
@@ -154,6 +190,25 @@ async function openNativePathWithIntent(
|
|
235
|
+
throw new Error(`native path opener is unsupported on ${platform}`)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
+/**
|
|
239
|
+
+ * Whether this host can reach a Linux desktop application.
|
|
240
|
+
+ *
|
|
241
|
+
+ * WSL hands every path to the Windows desktop, so a Linux GUI program such as
|
|
242
|
+
+ * `xdg-open` is not what opens anything there: a surface asking this question
|
|
243
|
+
+ * wants to know whether a Linux display is attached, and WSL is not one even
|
|
244
|
+
+ * though it can open a path.
|
|
245
|
+
+ *
|
|
246
|
+
+ * @param internals - platform and environment seam for deterministic tests.
|
|
247
|
+
+ * @returns true when a Linux desktop application can receive the path.
|
|
248
|
+
+ */
|
|
249
|
+
+export function canOpenLinuxDesktop(internals: PathOpenerInternals = {}): boolean {
|
|
250
|
+
+ const platform = internals.platform ?? process.platform
|
|
251
|
+
+ if (platform !== 'linux') return false
|
|
252
|
+
+ if (isWsl(internals)) return false
|
|
253
|
+
+ const env = internals.env ?? process.env
|
|
254
|
+
+ return present(env.DISPLAY) || present(env.WAYLAND_DISPLAY)
|
|
255
|
+
+}
|
|
256
|
+
+
|
|
257
|
+
/**
|
|
258
|
+
* Whether {@link openNativePath} plausibly reaches a desktop on this host.
|
|
259
|
+
*
|