@deepseek-ai/dsh-native-command 0.1.7-rc.1 → 0.2.0-rc.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.i18n.yaml +51 -5
- package/README.md +6 -5
- package/README.zh.md +6 -5
- package/lib/index.js +78 -34
- package/lib/types/file-applications-linux.js +2 -2
- package/lib/types/file-applications-windows.d.ts +2 -2
- package/lib/types/file-applications-windows.js +55 -10
- package/lib/types/file-applications.js +4 -4
- package/lib/types/path-opener.js +11 -10
- package/lib/types/runner.d.ts +4 -2
- package/lib/types/runner.js +5 -3
- package/package.json +1 -1
package/README.i18n.yaml
CHANGED
|
@@ -1,6 +1,52 @@
|
|
|
1
|
-
# Bilingual-pair consistency record (docs/i18n/README.md):
|
|
2
|
-
#
|
|
3
|
-
#
|
|
1
|
+
# Bilingual-pair consistency record for README.md (docs/i18n/README.md): per heading
|
|
2
|
+
# section, a hash of its English and Chinese blocks outside code blocks and generated regions.
|
|
3
|
+
# After editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/util/native-command/README.md
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/:
|
|
6
|
+
en: 3e0acbcc586810bb
|
|
7
|
+
zh: 688d34f126d1450e
|
|
8
|
+
/deepseek-ai-dsh-native-command:
|
|
9
|
+
en: 5ebca99c0488e1c8
|
|
10
|
+
zh: 24b2f8f341ff648d
|
|
11
|
+
/deepseek-ai-dsh-native-command/summary:
|
|
12
|
+
en: 8369dd3eb2af0781
|
|
13
|
+
zh: fa9dfd927cc8aaad
|
|
14
|
+
/deepseek-ai-dsh-native-command/table-of-contents:
|
|
15
|
+
en: d152484eb41ac6b4
|
|
16
|
+
zh: 09388d293f9be9cb
|
|
17
|
+
/deepseek-ai-dsh-native-command/use-this-package:
|
|
18
|
+
en: aba695cba233905f
|
|
19
|
+
zh: c6359da30eb8e372
|
|
20
|
+
/deepseek-ai-dsh-native-command/use-this-package/running-a-command:
|
|
21
|
+
en: 6dbc6c9bf6db34be
|
|
22
|
+
zh: 9ab16a86ca5abcc2
|
|
23
|
+
/deepseek-ai-dsh-native-command/use-this-package/injecting-the-command-boundary:
|
|
24
|
+
en: 987bed629737b59d
|
|
25
|
+
zh: 258e30f813b0edcb
|
|
26
|
+
/deepseek-ai-dsh-native-command/use-this-package/opening-a-host-path:
|
|
27
|
+
en: b4ec677605b12ff8
|
|
28
|
+
zh: f35256957eea141a
|
|
29
|
+
/deepseek-ai-dsh-native-command/understand-the-implementation:
|
|
30
|
+
en: 78e86818bea36f9f
|
|
31
|
+
zh: 71dc1ad3e4cbe9f1
|
|
32
|
+
/deepseek-ai-dsh-native-command/understand-the-implementation/source-map:
|
|
33
|
+
en: a19bb4cb9f6de785
|
|
34
|
+
zh: cc930d8facf1ca07
|
|
35
|
+
/deepseek-ai-dsh-native-command/understand-the-implementation/what-execfile-gives-the-runner:
|
|
36
|
+
en: ce6180ce8209db94
|
|
37
|
+
zh: 30000a1a073110e8
|
|
38
|
+
/deepseek-ai-dsh-native-command/further-exploration:
|
|
39
|
+
en: a3af562edb6e0385
|
|
40
|
+
zh: e26e2c311c07d1c5
|
|
41
|
+
/deepseek-ai-dsh-native-command/model-experience:
|
|
42
|
+
en: d478457568b9517e
|
|
43
|
+
zh: 0460368e7567a13f
|
|
44
|
+
/deepseek-ai-dsh-native-command/model-experience/kv-cache-effect:
|
|
45
|
+
en: 296c060e95197cc5
|
|
46
|
+
zh: b9d3fdf11b5ebe1e
|
|
47
|
+
/deepseek-ai-dsh-native-command/known-limitations-and-deferred-work:
|
|
48
|
+
en: d3af15e86d679b8e
|
|
49
|
+
zh: ee5aa3c8cbd9d2db
|
|
50
|
+
/deepseek-ai-dsh-native-command/known-limitations-and-deferred-work/dev-note:
|
|
51
|
+
en: e67f633dc6fe7773
|
|
52
|
+
zh: 3bb0c20c93f16f98
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
`dsh-native-command` runs host executables without a shell and opens Host filesystem paths through the desktop. The command runner captures utf8 output, propagates cancellation, and
|
|
12
|
+
`dsh-native-command` runs host executables without a shell and opens Host filesystem paths through the desktop. The command runner captures utf8 output, propagates cancellation, and requires explicit Windows startup visibility. The path opener supports default-application and text-editor intents, browser-renderable documents, WSL translation, and desktop availability checks. It is a library, not a plugin: no `ctx`, no state, no events.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -34,10 +34,10 @@ import { runNativeCommand } from '@deepseek-ai/dsh-native-command'
|
|
|
34
34
|
|
|
35
35
|
declare const script: string
|
|
36
36
|
declare const signal: AbortSignal
|
|
37
|
-
const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], signal)
|
|
37
|
+
const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], signal, 'hidden')
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
On exit 0 the call resolves with captured stdout and stderr. On any failure it rejects with the exit `code` and both captured streams attached, so a caller can tell a missing tool (`ENOENT`), a cancellation (`ABORT_ERR`), and a real command failure apart without re-running the command.
|
|
40
|
+
On exit 0 the call resolves with captured stdout and stderr. On any failure it rejects with the exit `code` and both captured streams attached, so a caller can tell a missing tool (`ENOENT`), a cancellation (`ABORT_ERR`), and a real command failure apart without re-running the command. The required fourth argument, `window: 'hidden' | 'visible'`, controls Windows startup visibility and has no effect on other platforms. Background commands pass `'hidden'`; GUI launchers pass `'visible'`. The path opener selects `'visible'` for Explorer, including WSL handoffs and file selection, because hiding Explorer also hides its folder window.
|
|
41
41
|
|
|
42
42
|
### Injecting the command boundary
|
|
43
43
|
|
|
@@ -49,7 +49,7 @@ The `NativeCommandRunner` type is the injectable command boundary for host integ
|
|
|
49
49
|
|
|
50
50
|
`revealNativePath(path, signal)` selects the file in Finder or Explorer, including WSL path translation, and opens its parent directory through `xdg-open` on desktop Linux. `nativeFileManager()` identifies that action for Host-derived UI labels; desktop availability remains a separate `canOpenNativePath()` check. Callers must authorize the absolute file path before invoking either operation. Platform dispatch is covered by injected-runner tests; native desktop verification belongs to the corresponding platform. Explorer receives an encoded file URI as a separate argument. Its exit code 1 is accepted as a delegated handoff; cancellation, missing executables, and other exit codes still reject. This acknowledgement does not prove that an application opened the file or that a window selected it.
|
|
51
51
|
|
|
52
|
-
`nativeFileApplications(path, signal)` returns registered applications, localized names, icons, and the current default. macOS 12 and later use LaunchServices; Windows uses Shell association handlers; Linux uses GIO with shared XDG desktop-entry and icon lookup. On macOS, copies sharing a bundle identifier and display name (staged self-updates, per-version installs) collapse to the system default, else the highest version; deliberate side-by-side installs keep distinct display names and both entries. `openNativeFileApplication(path, application, signal)` revalidates the handler against the complete registered list, so copies collapsed out of the display list stay openable, and never changes the system default. Windows delegates invocation to the Shell, and Linux delegates argument expansion to `gio launch`. WSL translates the path and uses the Windows adapter. Callers authorize the local file path. Native integration tests use private Windows file associations and Linux XDG roots on their respective platforms.
|
|
52
|
+
`nativeFileApplications(path, signal)` returns registered applications, localized names, icons, and the current default. macOS 12 and later use LaunchServices; Windows uses Shell association handlers and resolves a packaged application’s indirect icon reference before extraction; Linux uses GIO with shared XDG desktop-entry and icon lookup. On macOS, copies sharing a bundle identifier and display name (staged self-updates, per-version installs) collapse to the system default, else the highest version; deliberate side-by-side installs keep distinct display names and both entries. `openNativeFileApplication(path, application, signal)` revalidates the handler against the complete registered list, so copies collapsed out of the display list stay openable, and never changes the system default. Windows delegates invocation to the Shell, and Linux delegates argument expansion to `gio launch`. WSL translates the path and uses the Windows adapter. Callers authorize the local file path. Native integration tests use private Windows file associations and Linux XDG roots on their respective platforms.
|
|
53
53
|
|
|
54
54
|
-----
|
|
55
55
|
|
|
@@ -72,7 +72,7 @@ The command runner is a thin wrapper over Node's `execFile`. The path opener sel
|
|
|
72
72
|
|
|
73
73
|
### What execFile gives the runner
|
|
74
74
|
|
|
75
|
-
`execFile` spawns the executable directly with an argv array — no shell string, no shell interpretation of the arguments. The `signal` option terminates the child when the caller's abort fires;
|
|
75
|
+
`execFile` spawns the executable directly with an argv array — no shell string, no shell interpretation of the arguments. The `signal` option terminates the child when the caller's abort fires; the runner maps `window` to Node's `windowsHide` option. On a non-zero exit or spawn error, the callback attaches `code`, `stdout`, and `stderr` to the rejected error and keeps the original error as `cause`.
|
|
76
76
|
|
|
77
77
|
</details>
|
|
78
78
|
|
|
@@ -109,6 +109,7 @@ Linux association discovery and explicit launching require GIO. Missing native c
|
|
|
109
109
|
These limits define when this runner is not the right tool. They are current package constraints, not a task backlog.
|
|
110
110
|
|
|
111
111
|
- Command output uses Node’s `execFile` buffer limit; oversized replies reject. Use the subprocess capability for streaming output.
|
|
112
|
+
- A Windows handler icon appears only when the Shell resolves its indirect reference; an unresolved reference falls back to the handler executable, and a handler that names none — a packaged handler may report an AUMID or family name — yields no icon.
|
|
112
113
|
- A Windows host needs an interactive desktop session. In a non-interactive session — a service, or a scheduled task without an interactive logon — `explorer.exe` invokes no file association and still exits 1 after about a second (an SSH session 0 on Windows 11 ARM64 build 26200), so an open reports success having opened nothing; a caller's abort signal turns that wait into a timeout rather than a hang. An `Invoke-Item` command resolves the association in-process there, and its directory failure arrives as a non-zero exit code. No fallback for that session is implemented.
|
|
113
114
|
|
|
114
115
|
<a id="dev-note"></a>
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-library"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
`dsh-native-command` 无需 shell 即可运行 Host 可执行文件,并通过桌面打开 Host 文件系统路径。命令运行器捕获 utf8
|
|
12
|
+
`dsh-native-command` 无需 shell 即可运行 Host 可执行文件,并通过桌面打开 Host 文件系统路径。命令运行器捕获 utf8 输出、传播取消,并要求显式指定 Windows 启动时的窗口可见性。路径打开器支持默认应用与文本编辑器意图、浏览器可渲染文档、WSL 转换与桌面可用性检查。它是库而非插件:没有 `ctx`、无状态、不发事件。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -34,10 +34,10 @@ import { runNativeCommand } from '@deepseek-ai/dsh-native-command'
|
|
|
34
34
|
|
|
35
35
|
declare const script: string
|
|
36
36
|
declare const signal: AbortSignal
|
|
37
|
-
const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], signal)
|
|
37
|
+
const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], signal, 'hidden')
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
退出码为 0 时,调用解析为捕获到的 stdout 与 stderr。任何失败都会以错误拒绝,错误附带退出 `code` 与两路已捕获输出,因此调用方无需重跑命令即可区分工具缺失(`ENOENT`)、取消(`ABORT_ERR
|
|
40
|
+
退出码为 0 时,调用解析为捕获到的 stdout 与 stderr。任何失败都会以错误拒绝,错误附带退出 `code` 与两路已捕获输出,因此调用方无需重跑命令即可区分工具缺失(`ENOENT`)、取消(`ABORT_ERR`)与真实的命令失败。必填的第四个参数 `window: 'hidden' | 'visible'` 控制 Windows 启动时的窗口可见性,在其他平台不生效。后台命令传入 `'hidden'`,GUI 启动程序传入 `'visible'`。路径打开器为 Explorer 选择 `'visible'`,包括 WSL 转交和文件选中操作,因为隐藏 Explorer 也会隐藏它的文件夹窗口。
|
|
41
41
|
|
|
42
42
|
### 注入命令边界
|
|
43
43
|
|
|
@@ -49,7 +49,7 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s
|
|
|
49
49
|
|
|
50
50
|
`revealNativePath(path, signal)` 在 Finder 或文件资源管理器中选中文件,包含 WSL 路径转换;在桌面 Linux 上通过 `xdg-open` 打开上层目录。`nativeFileManager()` 标识该操作,供 UI 根据 Host 选择文案;桌面是否可用仍由独立的 `canOpenNativePath()` 检查决定。调用方必须先授权绝对文件路径,再执行操作。平台分派由注入运行器的测试覆盖;原生桌面验证由对应平台负责。 Explorer 接收独立参数中的编码文件 URI。退出码 1 按已转交请求处理;取消、找不到可执行文件和其他退出码仍然报错。该确认不能证明应用已打开文件,也不能证明桌面窗口已选中文件。
|
|
51
51
|
|
|
52
|
-
`nativeFileApplications(path, signal)` 返回关联应用、本地化名称、图标和当前默认项。macOS 12 及以上版本使用 LaunchServices,Windows 使用 Shell
|
|
52
|
+
`nativeFileApplications(path, signal)` 返回关联应用、本地化名称、图标和当前默认项。macOS 12 及以上版本使用 LaunchServices,Windows 使用 Shell 关联处理器并在提取前解析打包应用的间接图标引用,Linux 使用 GIO,并共用 XDG 桌面文件和图标读取逻辑。macOS 上 bundle 标识符与显示名都相同的多份拷贝(自更新暂存副本、按版本安装的拷贝)合并为系统默认项,否则保留最高版本;显示名不同的并存安装两项都保留。`openNativeFileApplication(path, application, signal)` 按完整的当前注册列表重新验证关联应用,展示列表中被合并掉的拷贝仍可打开,且不修改系统默认值。Windows 交给 Shell 启动应用,Linux 交给 `gio launch` 展开参数。WSL 转换路径后使用 Windows 适配器。调用方负责验证本地文件路径。原生集成测试分别在相应平台使用独立的 Windows 文件关联和 Linux XDG 目录。
|
|
53
53
|
|
|
54
54
|
-----
|
|
55
55
|
|
|
@@ -72,7 +72,7 @@ const { stdout, stderr } = await runNativeCommand('osascript', ['-e', script], s
|
|
|
72
72
|
|
|
73
73
|
### execFile 给了运行器什么
|
|
74
74
|
|
|
75
|
-
`execFile` 以 argv 数组直接 spawn 可执行文件——没有 shell 字符串,参数不经 shell 解释。`signal` 选项在调用方中止触发时终止子进程;`
|
|
75
|
+
`execFile` 以 argv 数组直接 spawn 可执行文件——没有 shell 字符串,参数不经 shell 解释。`signal` 选项在调用方中止触发时终止子进程;`window` 由运行器映射为 Node 的 `windowsHide` 选项。遇到非零退出或 spawn 错误时,回调把 `code`、`stdout`、`stderr` 挂到被拒绝的错误上,并保留原始错误作为 `cause`。
|
|
76
76
|
|
|
77
77
|
</details>
|
|
78
78
|
|
|
@@ -109,6 +109,7 @@ Linux 的关联查询和指定应用启动需要 GIO。缺少原生命令时查
|
|
|
109
109
|
这些限制说明本运行器何时不是合适的工具。它们是当前包约束,不是任务积压。
|
|
110
110
|
|
|
111
111
|
- 命令输出受 Node 的 `execFile` 缓冲上限约束,超过上限时拒绝结果。流式输出由 subprocess 能力处理。
|
|
112
|
+
- Windows 关联处理器的图标只在 Shell 能解析其间接引用时显示;解析失败时回退到处理器的可执行文件,处理器没有给出可执行文件(打包处理器可能只报 AUMID 或家族名)时则没有图标。
|
|
112
113
|
- Windows 主机需要交互桌面会话。在非交互会话(服务,或没有交互登录的计划任务)中,`explorer.exe` 不调用任何文件关联,且仍在约 1 秒后以退出码 1 返回(Windows 11 ARM64 build 26200 的 SSH session 0 实测),于是「打开」会报告成功却什么都没打开;调用方的 abort 信号把这段等待变成超时而不是挂死。同一目标改用 `Invoke-Item` 命令时可在该会话内解析关联,目录失败也会以非零退出码报出。该会话下的兜底尚未实现。
|
|
113
114
|
|
|
114
115
|
<a id="dev-note"></a>
|
package/lib/index.js
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { homedir, release } from "node:os";
|
|
2
|
+
import { homedir, release, tmpdir } from "node:os";
|
|
3
3
|
import { dirname, extname, isAbsolute, join, relative, sep } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
5
|
+
import { mkdtemp, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
6
6
|
//#region lib/types/runner.js
|
|
7
7
|
/**
|
|
8
8
|
* Shared no-shell `execFile` runner for host-native OS integrations.
|
|
9
9
|
* @module @deepseek-ai/dsh-native-command/runner
|
|
10
10
|
*/
|
|
11
11
|
/**
|
|
12
|
-
* Run a host command with utf8 stdio, abort propagation, and
|
|
12
|
+
* Run a host command with utf8 stdio, abort propagation, and explicit GUI visibility.
|
|
13
13
|
* @param command - executable path or PATH name.
|
|
14
14
|
* @param args - argv (never a shell string).
|
|
15
15
|
* @param signal - caller/connection lifetime; abort terminates the child.
|
|
16
|
+
* @param window - Windows startup visibility: `hidden` for background commands,
|
|
17
|
+
* `visible` for GUI launchers. Ignored on other platforms.
|
|
16
18
|
* @returns captured stdout/stderr on exit 0.
|
|
17
19
|
*/
|
|
18
|
-
const runNativeCommand = (command, args, signal) => new Promise((resolve, reject) => {
|
|
20
|
+
const runNativeCommand = (command, args, signal, window) => new Promise((resolve, reject) => {
|
|
19
21
|
execFile(command, [...args], {
|
|
20
22
|
encoding: "utf8",
|
|
21
23
|
signal,
|
|
22
|
-
windowsHide:
|
|
24
|
+
windowsHide: window === "hidden"
|
|
23
25
|
}, (error, stdout, stderr) => {
|
|
24
26
|
if (error !== null) {
|
|
25
27
|
reject(Object.assign(new Error(error.message, { cause: error }), {
|
|
@@ -77,7 +79,7 @@ async function openInBrowser(path, signal, platform, run, env) {
|
|
|
77
79
|
if (platform === "darwin") {
|
|
78
80
|
let bundle;
|
|
79
81
|
try {
|
|
80
|
-
const { stdout } = await run("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure"], signal);
|
|
82
|
+
const { stdout } = await run("defaults", ["read", "com.apple.LaunchServices/com.apple.launchservices.secure"], signal, "hidden");
|
|
81
83
|
bundle = macBundleForHttps(stdout);
|
|
82
84
|
} catch {
|
|
83
85
|
return false;
|
|
@@ -87,13 +89,13 @@ async function openInBrowser(path, signal, platform, run, env) {
|
|
|
87
89
|
"-b",
|
|
88
90
|
bundle,
|
|
89
91
|
path
|
|
90
|
-
], signal);
|
|
92
|
+
], signal, "hidden");
|
|
91
93
|
return true;
|
|
92
94
|
}
|
|
93
95
|
if (platform === "linux") {
|
|
94
96
|
const browser = env.BROWSER;
|
|
95
97
|
if (browser === void 0 || browser === "") return false;
|
|
96
|
-
await run(browser, [path], signal);
|
|
98
|
+
await run(browser, [path], signal, "hidden");
|
|
97
99
|
return true;
|
|
98
100
|
}
|
|
99
101
|
return false;
|
|
@@ -140,7 +142,7 @@ function explorerTarget(windowsPath) {
|
|
|
140
142
|
*/
|
|
141
143
|
async function runExplorer(args, signal, run) {
|
|
142
144
|
try {
|
|
143
|
-
await run("explorer.exe", args, signal);
|
|
145
|
+
await run("explorer.exe", args, signal, "visible");
|
|
144
146
|
} catch (error) {
|
|
145
147
|
signal.throwIfAborted();
|
|
146
148
|
if (!(error instanceof Error) || !("code" in error) || error.code !== 1) throw error;
|
|
@@ -158,7 +160,7 @@ async function openWindowsPath(path, signal, run) {
|
|
|
158
160
|
}
|
|
159
161
|
/** Translate a WSL path before handing it to the Windows desktop. */
|
|
160
162
|
async function openWslPath(path, signal, run) {
|
|
161
|
-
const translated = await run("wslpath", ["-w", path], signal);
|
|
163
|
+
const translated = await run("wslpath", ["-w", path], signal, "hidden");
|
|
162
164
|
signal.throwIfAborted();
|
|
163
165
|
const windowsPath = translated.stdout.replace(/[\r\n]+$/, "");
|
|
164
166
|
if (windowsPath === "") throw new Error("wslpath returned no Windows path");
|
|
@@ -172,7 +174,7 @@ async function openNativePathWithIntent(path, signal, intent, internals = {}) {
|
|
|
172
174
|
const wsl = platform === "linux" && isWsl(internals);
|
|
173
175
|
if (!wsl && intent === "default" && BROWSER_DOCUMENTS.has(extname(path).toLowerCase()) && await openInBrowser(path, signal, platform, run, env)) return;
|
|
174
176
|
if (platform === "darwin") {
|
|
175
|
-
await run("open", intent === "text-editor" ? ["-t", path] : [path], signal);
|
|
177
|
+
await run("open", intent === "text-editor" ? ["-t", path] : [path], signal, "hidden");
|
|
176
178
|
return;
|
|
177
179
|
}
|
|
178
180
|
if (platform === "win32") {
|
|
@@ -184,7 +186,7 @@ async function openNativePathWithIntent(path, signal, intent, internals = {}) {
|
|
|
184
186
|
await openWslPath(path, signal, run);
|
|
185
187
|
return;
|
|
186
188
|
}
|
|
187
|
-
await run("xdg-open", [path], signal);
|
|
189
|
+
await run("xdg-open", [path], signal, "hidden");
|
|
188
190
|
return;
|
|
189
191
|
}
|
|
190
192
|
throw new Error(`native path opener is unsupported on ${platform}`);
|
|
@@ -264,13 +266,13 @@ async function revealNativePath(path, signal, internals = {}) {
|
|
|
264
266
|
platform
|
|
265
267
|
});
|
|
266
268
|
if (manager === "finder") {
|
|
267
|
-
await run("open", ["-R", path], signal);
|
|
269
|
+
await run("open", ["-R", path], signal, "hidden");
|
|
268
270
|
return;
|
|
269
271
|
}
|
|
270
272
|
if (manager === "explorer") {
|
|
271
273
|
let windowsPath = path;
|
|
272
274
|
if (platform === "linux") {
|
|
273
|
-
const translated = await run("wslpath", ["-w", path], signal);
|
|
275
|
+
const translated = await run("wslpath", ["-w", path], signal, "hidden");
|
|
274
276
|
signal.throwIfAborted();
|
|
275
277
|
windowsPath = translated.stdout.replace(/[\r\n]+$/, "");
|
|
276
278
|
if (windowsPath === "") throw new Error("wslpath returned no Windows path");
|
|
@@ -279,7 +281,7 @@ async function revealNativePath(path, signal, internals = {}) {
|
|
|
279
281
|
return;
|
|
280
282
|
}
|
|
281
283
|
if (manager === "directory") {
|
|
282
|
-
await run("xdg-open", [dirname(path)], signal);
|
|
284
|
+
await run("xdg-open", [dirname(path)], signal, "hidden");
|
|
283
285
|
return;
|
|
284
286
|
}
|
|
285
287
|
throw new Error(`native file manager is unsupported on ${platform}`);
|
|
@@ -394,7 +396,7 @@ async function linuxFileApplications(path, signal, run, env) {
|
|
|
394
396
|
"-a",
|
|
395
397
|
"standard::content-type",
|
|
396
398
|
path
|
|
397
|
-
], signal);
|
|
399
|
+
], signal, "hidden");
|
|
398
400
|
const mime = /standard::content-type:\s*(\S+)/.exec(info.stdout)?.[1];
|
|
399
401
|
if (mime === void 0) throw new Error("GIO did not identify the file content type");
|
|
400
402
|
const result = await run("env", [
|
|
@@ -402,7 +404,7 @@ async function linuxFileApplications(path, signal, run, env) {
|
|
|
402
404
|
"gio",
|
|
403
405
|
"mime",
|
|
404
406
|
mime
|
|
405
|
-
], signal);
|
|
407
|
+
], signal, "hidden");
|
|
406
408
|
const preferred = /^Default application.*:\s*(.+\.desktop)\s*$/m.exec(result.stdout)?.[1];
|
|
407
409
|
const ids = [...new Set([...preferred === void 0 ? [] : [preferred], ...result.stdout.split(/\r?\n/).filter((line) => /^\s+.*\.desktop\s*$/.test(line)).map((line) => line.trim())])];
|
|
408
410
|
const directories = desktopDataDirectories(env.HOME ?? homedir(), env);
|
|
@@ -431,6 +433,7 @@ async function linuxFileApplications(path, signal, run, env) {
|
|
|
431
433
|
}
|
|
432
434
|
//#endregion
|
|
433
435
|
//#region lib/types/file-applications-windows.js
|
|
436
|
+
/** Windows Shell association queries and invocation; paths are encoded data, never PowerShell expressions. */
|
|
434
437
|
/** Shell interfaces are declared in their native vtable order; Invoke preserves packaged-app and DDE handling. */
|
|
435
438
|
const WINDOWS_ASSOCIATIONS = String.raw`
|
|
436
439
|
using System;
|
|
@@ -471,6 +474,8 @@ public static class DshFileAssociations {
|
|
|
471
474
|
static extern int SHDefExtractIcon(string path, int index, uint flags, out IntPtr large, out IntPtr small, uint size);
|
|
472
475
|
[DllImport("user32.dll")]
|
|
473
476
|
static extern bool DestroyIcon(IntPtr icon);
|
|
477
|
+
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
|
|
478
|
+
static extern int SHLoadIndirectString(string source, StringBuilder output, uint size, IntPtr reserved);
|
|
474
479
|
|
|
475
480
|
[DllImport("shlwapi.dll", PreserveSig = false)]
|
|
476
481
|
static extern void SHCreateThreadRef(IntPtr count, out IntPtr reference);
|
|
@@ -548,12 +553,18 @@ public static class DshFileAssociations {
|
|
|
548
553
|
var text = new StringBuilder((int)size);
|
|
549
554
|
return AssocQueryString(0, kind, extension, null, text, ref size) == 0 ? text.ToString() : null;
|
|
550
555
|
}
|
|
551
|
-
|
|
556
|
+
// Resolve a packaged application's indirect icon reference, for example
|
|
557
|
+
// @{Microsoft.WindowsNotepad_...?ms-resource://.../NotepadAppList.png}, to
|
|
558
|
+
// the resource file the Shell would draw; an unresolved reference is null.
|
|
559
|
+
static string ResolveIndirectIcon(string source) {
|
|
560
|
+
var output = new StringBuilder(1024);
|
|
561
|
+
return SHLoadIndirectString(source, output, (uint)output.Capacity, IntPtr.Zero) == 0 ? output.ToString() : null;
|
|
562
|
+
}
|
|
563
|
+
// Read one icon source into a 32px PNG: an image file directly, otherwise the
|
|
564
|
+
// resource the Shell extracts at the given index. A missing resource is null.
|
|
565
|
+
static string PngFromIconSource(string source, int index) {
|
|
552
566
|
IntPtr large = IntPtr.Zero, small = IntPtr.Zero;
|
|
553
567
|
try {
|
|
554
|
-
string source; int index;
|
|
555
|
-
handler.GetIconLocation(out source, out index);
|
|
556
|
-
source = Environment.ExpandEnvironmentVariables(source);
|
|
557
568
|
if (Path.GetExtension(source).Equals(".png", StringComparison.OrdinalIgnoreCase)) {
|
|
558
569
|
using (var original = Image.FromFile(source))
|
|
559
570
|
using (var resized = new Bitmap(original, new Size(32, 32)))
|
|
@@ -577,6 +588,26 @@ public static class DshFileAssociations {
|
|
|
577
588
|
if (small != IntPtr.Zero) DestroyIcon(small);
|
|
578
589
|
}
|
|
579
590
|
}
|
|
591
|
+
// Best-effort fallback: some handlers return their executable path from GetName, but a
|
|
592
|
+
// packaged handler's name may be an AUMID or family name, which extracts no icon.
|
|
593
|
+
static string IconData(IHandler handler, string id) {
|
|
594
|
+
string source = null; int index = 0;
|
|
595
|
+
try { handler.GetIconLocation(out source, out index); } catch (Exception) {
|
|
596
|
+
// A handler without a drawable icon location falls back to its name.
|
|
597
|
+
}
|
|
598
|
+
if (!String.IsNullOrEmpty(source)) {
|
|
599
|
+
source = Environment.ExpandEnvironmentVariables(source);
|
|
600
|
+
if (source.StartsWith("@{", StringComparison.Ordinal) && source.EndsWith("}", StringComparison.Ordinal)) {
|
|
601
|
+
var resolved = ResolveIndirectIcon(source);
|
|
602
|
+
if (!String.IsNullOrEmpty(resolved)) source = resolved;
|
|
603
|
+
}
|
|
604
|
+
var icon = PngFromIconSource(source, index);
|
|
605
|
+
if (icon != null) return icon;
|
|
606
|
+
}
|
|
607
|
+
return String.IsNullOrEmpty(id) || String.Equals(id, source, StringComparison.OrdinalIgnoreCase)
|
|
608
|
+
? null
|
|
609
|
+
: PngFromIconSource(id, 0);
|
|
610
|
+
}
|
|
580
611
|
static void Visit(string path, Action<IHandler> visit) {
|
|
581
612
|
var extension = Path.GetExtension(path);
|
|
582
613
|
if (extension.Length == 0) return;
|
|
@@ -602,7 +633,7 @@ public static class DshFileAssociations {
|
|
|
602
633
|
string id, name;
|
|
603
634
|
handler.GetName(out id); handler.GetUIName(out name);
|
|
604
635
|
if (!seen.Add(id)) return;
|
|
605
|
-
apps.Add(new Application { id = id, name = name, icon = IconData(handler),
|
|
636
|
+
apps.Add(new Application { id = id, name = name, icon = IconData(handler, id),
|
|
606
637
|
@default = String.Equals(id, executable, StringComparison.OrdinalIgnoreCase) || String.Equals(id, appId, StringComparison.OrdinalIgnoreCase) });
|
|
607
638
|
});
|
|
608
639
|
return apps.ToArray();
|
|
@@ -634,7 +665,8 @@ public static class DshFileAssociations {
|
|
|
634
665
|
}
|
|
635
666
|
}`;
|
|
636
667
|
/**
|
|
637
|
-
* Execute the Windows Shell adapter in a Unicode STA PowerShell process
|
|
668
|
+
* Execute the Windows Shell adapter in a Unicode STA PowerShell process; the adapter
|
|
669
|
+
* source is a private temporary script file, removed once the call settles.
|
|
638
670
|
* @param path - Windows file path, translated by the caller for WSL.
|
|
639
671
|
* @param application - registered handler to invoke; null requests the application list.
|
|
640
672
|
* @param signal - caller cancellation.
|
|
@@ -651,13 +683,25 @@ $path = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${Buffer.fr
|
|
|
651
683
|
$application = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${Buffer.from(application ?? "").toString("base64")}'))
|
|
652
684
|
${application === null ? "ConvertTo-Json -InputObject @([DshFileAssociations]::List($path)) -Depth 4 -Compress" : "[DshFileAssociations]::Open($path, $application)"}
|
|
653
685
|
`;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
"
|
|
658
|
-
"
|
|
659
|
-
|
|
660
|
-
|
|
686
|
+
const directory = await mkdtemp(join(tmpdir(), "dsh-native-command-"));
|
|
687
|
+
const scriptPath = join(directory, "associations.ps1");
|
|
688
|
+
try {
|
|
689
|
+
await writeFile(scriptPath, `\uFEFF${script}`, "utf8");
|
|
690
|
+
return (await run("powershell.exe", [
|
|
691
|
+
"-NoProfile",
|
|
692
|
+
"-NonInteractive",
|
|
693
|
+
"-STA",
|
|
694
|
+
"-ExecutionPolicy",
|
|
695
|
+
"Bypass",
|
|
696
|
+
"-File",
|
|
697
|
+
scriptPath
|
|
698
|
+
], signal, "hidden")).stdout;
|
|
699
|
+
} finally {
|
|
700
|
+
await rm(directory, {
|
|
701
|
+
recursive: true,
|
|
702
|
+
force: true
|
|
703
|
+
});
|
|
704
|
+
}
|
|
661
705
|
}
|
|
662
706
|
//#endregion
|
|
663
707
|
//#region lib/types/types.js
|
|
@@ -752,7 +796,7 @@ async function queryFileApplications(path, signal, internals, display) {
|
|
|
752
796
|
MAC_APPLICATIONS,
|
|
753
797
|
target.path,
|
|
754
798
|
display ? "icons" : "handlers"
|
|
755
|
-
], signal);
|
|
799
|
+
], signal, "hidden");
|
|
756
800
|
const applications = parseMacApplications(JSON.parse(stdout));
|
|
757
801
|
return display ? dedupeMacApplications(applications) : applications;
|
|
758
802
|
}
|
|
@@ -856,19 +900,19 @@ async function openNativeFileApplication(path, application, signal, internals =
|
|
|
856
900
|
"launch",
|
|
857
901
|
application,
|
|
858
902
|
path
|
|
859
|
-
], signal);
|
|
903
|
+
], signal, "hidden");
|
|
860
904
|
else await run("open", [
|
|
861
905
|
"-a",
|
|
862
906
|
application,
|
|
863
907
|
path
|
|
864
|
-
], signal);
|
|
908
|
+
], signal, "hidden");
|
|
865
909
|
}
|
|
866
910
|
/** Resolve the desktop that owns the file, including Windows applications reached from WSL. */
|
|
867
911
|
async function desktopTarget(path, signal, internals) {
|
|
868
912
|
signal.throwIfAborted();
|
|
869
913
|
const platform = internals.platform ?? process.platform;
|
|
870
914
|
if (platform === "linux" && nativeFileManager(internals) === "explorer") {
|
|
871
|
-
const translated = await (internals.run ?? runNativeCommand)("wslpath", ["-w", path], signal);
|
|
915
|
+
const translated = await (internals.run ?? runNativeCommand)("wslpath", ["-w", path], signal, "hidden");
|
|
872
916
|
signal.throwIfAborted();
|
|
873
917
|
const windowsPath = translated.stdout.replace(/[\r\n]+$/, "");
|
|
874
918
|
if (windowsPath === "") throw new Error("wslpath returned no Windows path");
|
|
@@ -36,11 +36,11 @@ async function desktopFile(root, id, directory = root) {
|
|
|
36
36
|
* @returns registered applications with the desktop's default marked.
|
|
37
37
|
*/
|
|
38
38
|
export async function linuxFileApplications(path, signal, run, env) {
|
|
39
|
-
const info = await run('gio', ['info', '-a', 'standard::content-type', path], signal);
|
|
39
|
+
const info = await run('gio', ['info', '-a', 'standard::content-type', path], signal, 'hidden');
|
|
40
40
|
const mime = /standard::content-type:\s*(\S+)/.exec(info.stdout)?.[1];
|
|
41
41
|
if (mime === undefined)
|
|
42
42
|
throw new Error('GIO did not identify the file content type');
|
|
43
|
-
const result = await run('env', ['LC_ALL=C', 'gio', 'mime', mime], signal);
|
|
43
|
+
const result = await run('env', ['LC_ALL=C', 'gio', 'mime', mime], signal, 'hidden');
|
|
44
44
|
const preferred = /^Default application.*:\s*(.+\.desktop)\s*$/m.exec(result.stdout)?.[1];
|
|
45
45
|
const ids = [...new Set([...(preferred === undefined ? [] : [preferred]), ...result.stdout.split(/\r?\n/)
|
|
46
46
|
.filter(line => /^\s+.*\.desktop\s*$/.test(line)).map(line => line.trim())])];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
/** Windows Shell association queries and invocation; paths are encoded data, never PowerShell expressions. */
|
|
2
1
|
import type { NativeCommandRunner } from './runner.ts';
|
|
3
2
|
/**
|
|
4
|
-
* Execute the Windows Shell adapter in a Unicode STA PowerShell process
|
|
3
|
+
* Execute the Windows Shell adapter in a Unicode STA PowerShell process; the adapter
|
|
4
|
+
* source is a private temporary script file, removed once the call settles.
|
|
5
5
|
* @param path - Windows file path, translated by the caller for WSL.
|
|
6
6
|
* @param application - registered handler to invoke; null requests the application list.
|
|
7
7
|
* @param signal - caller cancellation.
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Windows Shell association queries and invocation; paths are encoded data, never PowerShell expressions. */
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
1
5
|
/** Shell interfaces are declared in their native vtable order; Invoke preserves packaged-app and DDE handling. */
|
|
2
6
|
const WINDOWS_ASSOCIATIONS = String.raw `
|
|
3
7
|
using System;
|
|
@@ -38,6 +42,8 @@ public static class DshFileAssociations {
|
|
|
38
42
|
static extern int SHDefExtractIcon(string path, int index, uint flags, out IntPtr large, out IntPtr small, uint size);
|
|
39
43
|
[DllImport("user32.dll")]
|
|
40
44
|
static extern bool DestroyIcon(IntPtr icon);
|
|
45
|
+
[DllImport("shlwapi.dll", CharSet = CharSet.Unicode)]
|
|
46
|
+
static extern int SHLoadIndirectString(string source, StringBuilder output, uint size, IntPtr reserved);
|
|
41
47
|
|
|
42
48
|
[DllImport("shlwapi.dll", PreserveSig = false)]
|
|
43
49
|
static extern void SHCreateThreadRef(IntPtr count, out IntPtr reference);
|
|
@@ -115,12 +121,18 @@ public static class DshFileAssociations {
|
|
|
115
121
|
var text = new StringBuilder((int)size);
|
|
116
122
|
return AssocQueryString(0, kind, extension, null, text, ref size) == 0 ? text.ToString() : null;
|
|
117
123
|
}
|
|
118
|
-
|
|
124
|
+
// Resolve a packaged application's indirect icon reference, for example
|
|
125
|
+
// @{Microsoft.WindowsNotepad_...?ms-resource://.../NotepadAppList.png}, to
|
|
126
|
+
// the resource file the Shell would draw; an unresolved reference is null.
|
|
127
|
+
static string ResolveIndirectIcon(string source) {
|
|
128
|
+
var output = new StringBuilder(1024);
|
|
129
|
+
return SHLoadIndirectString(source, output, (uint)output.Capacity, IntPtr.Zero) == 0 ? output.ToString() : null;
|
|
130
|
+
}
|
|
131
|
+
// Read one icon source into a 32px PNG: an image file directly, otherwise the
|
|
132
|
+
// resource the Shell extracts at the given index. A missing resource is null.
|
|
133
|
+
static string PngFromIconSource(string source, int index) {
|
|
119
134
|
IntPtr large = IntPtr.Zero, small = IntPtr.Zero;
|
|
120
135
|
try {
|
|
121
|
-
string source; int index;
|
|
122
|
-
handler.GetIconLocation(out source, out index);
|
|
123
|
-
source = Environment.ExpandEnvironmentVariables(source);
|
|
124
136
|
if (Path.GetExtension(source).Equals(".png", StringComparison.OrdinalIgnoreCase)) {
|
|
125
137
|
using (var original = Image.FromFile(source))
|
|
126
138
|
using (var resized = new Bitmap(original, new Size(32, 32)))
|
|
@@ -144,6 +156,26 @@ public static class DshFileAssociations {
|
|
|
144
156
|
if (small != IntPtr.Zero) DestroyIcon(small);
|
|
145
157
|
}
|
|
146
158
|
}
|
|
159
|
+
// Best-effort fallback: some handlers return their executable path from GetName, but a
|
|
160
|
+
// packaged handler's name may be an AUMID or family name, which extracts no icon.
|
|
161
|
+
static string IconData(IHandler handler, string id) {
|
|
162
|
+
string source = null; int index = 0;
|
|
163
|
+
try { handler.GetIconLocation(out source, out index); } catch (Exception) {
|
|
164
|
+
// A handler without a drawable icon location falls back to its name.
|
|
165
|
+
}
|
|
166
|
+
if (!String.IsNullOrEmpty(source)) {
|
|
167
|
+
source = Environment.ExpandEnvironmentVariables(source);
|
|
168
|
+
if (source.StartsWith("@{", StringComparison.Ordinal) && source.EndsWith("}", StringComparison.Ordinal)) {
|
|
169
|
+
var resolved = ResolveIndirectIcon(source);
|
|
170
|
+
if (!String.IsNullOrEmpty(resolved)) source = resolved;
|
|
171
|
+
}
|
|
172
|
+
var icon = PngFromIconSource(source, index);
|
|
173
|
+
if (icon != null) return icon;
|
|
174
|
+
}
|
|
175
|
+
return String.IsNullOrEmpty(id) || String.Equals(id, source, StringComparison.OrdinalIgnoreCase)
|
|
176
|
+
? null
|
|
177
|
+
: PngFromIconSource(id, 0);
|
|
178
|
+
}
|
|
147
179
|
static void Visit(string path, Action<IHandler> visit) {
|
|
148
180
|
var extension = Path.GetExtension(path);
|
|
149
181
|
if (extension.Length == 0) return;
|
|
@@ -169,7 +201,7 @@ public static class DshFileAssociations {
|
|
|
169
201
|
string id, name;
|
|
170
202
|
handler.GetName(out id); handler.GetUIName(out name);
|
|
171
203
|
if (!seen.Add(id)) return;
|
|
172
|
-
apps.Add(new Application { id = id, name = name, icon = IconData(handler),
|
|
204
|
+
apps.Add(new Application { id = id, name = name, icon = IconData(handler, id),
|
|
173
205
|
@default = String.Equals(id, executable, StringComparison.OrdinalIgnoreCase) || String.Equals(id, appId, StringComparison.OrdinalIgnoreCase) });
|
|
174
206
|
});
|
|
175
207
|
return apps.ToArray();
|
|
@@ -201,7 +233,8 @@ public static class DshFileAssociations {
|
|
|
201
233
|
}
|
|
202
234
|
}`;
|
|
203
235
|
/**
|
|
204
|
-
* Execute the Windows Shell adapter in a Unicode STA PowerShell process
|
|
236
|
+
* Execute the Windows Shell adapter in a Unicode STA PowerShell process; the adapter
|
|
237
|
+
* source is a private temporary script file, removed once the call settles.
|
|
205
238
|
* @param path - Windows file path, translated by the caller for WSL.
|
|
206
239
|
* @param application - registered handler to invoke; null requests the application list.
|
|
207
240
|
* @param signal - caller cancellation.
|
|
@@ -220,9 +253,21 @@ $path = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedPa
|
|
|
220
253
|
$application = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${encodedApplication}'))
|
|
221
254
|
${application === null ? 'ConvertTo-Json -InputObject @([DshFileAssociations]::List($path)) -Depth 4 -Compress' : '[DshFileAssociations]::Open($path, $application)'}
|
|
222
255
|
`;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
256
|
+
// The script travels as a file, not as command-line data: -EncodedCommand grew with the
|
|
257
|
+
// embedded C# and overflowed the 32767-character CreateProcess limit for long paths.
|
|
258
|
+
// PowerShell 5.1 decodes a -File script as ANSI unless it carries a BOM; the BOM keeps
|
|
259
|
+
// the embedded UTF-8 source intact.
|
|
260
|
+
const directory = await mkdtemp(join(tmpdir(), 'dsh-native-command-'));
|
|
261
|
+
const scriptPath = join(directory, 'associations.ps1');
|
|
262
|
+
try {
|
|
263
|
+
await writeFile(scriptPath, `\uFEFF${script}`, 'utf8');
|
|
264
|
+
const result = await run('powershell.exe', [
|
|
265
|
+
'-NoProfile', '-NonInteractive', '-STA', '-ExecutionPolicy', 'Bypass', '-File', scriptPath,
|
|
266
|
+
], signal, 'hidden');
|
|
267
|
+
return result.stdout;
|
|
268
|
+
}
|
|
269
|
+
finally {
|
|
270
|
+
await rm(directory, { recursive: true, force: true });
|
|
271
|
+
}
|
|
227
272
|
}
|
|
228
273
|
//# sourceMappingURL=file-applications-windows.js.map
|
|
@@ -64,7 +64,7 @@ async function queryFileApplications(path, signal, internals, display) {
|
|
|
64
64
|
if (target.platform === 'linux')
|
|
65
65
|
return linuxFileApplications(path, signal, run, internals.env ?? process.env);
|
|
66
66
|
if (target.platform === 'darwin') {
|
|
67
|
-
const { stdout } = await run('osascript', ['-l', 'JavaScript', '-e', MAC_APPLICATIONS, target.path, display ? 'icons' : 'handlers'], signal);
|
|
67
|
+
const { stdout } = await run('osascript', ['-l', 'JavaScript', '-e', MAC_APPLICATIONS, target.path, display ? 'icons' : 'handlers'], signal, 'hidden');
|
|
68
68
|
const applications = parseMacApplications(JSON.parse(stdout));
|
|
69
69
|
return display ? dedupeMacApplications(applications) : applications;
|
|
70
70
|
}
|
|
@@ -170,16 +170,16 @@ export async function openNativeFileApplication(path, application, signal, inter
|
|
|
170
170
|
if (!apps.some(app => app.id === application))
|
|
171
171
|
throw new Error('Application is not registered for this file');
|
|
172
172
|
if (target.platform === 'linux')
|
|
173
|
-
await run('gio', ['launch', application, path], signal);
|
|
173
|
+
await run('gio', ['launch', application, path], signal, 'hidden');
|
|
174
174
|
else
|
|
175
|
-
await run('open', ['-a', application, path], signal);
|
|
175
|
+
await run('open', ['-a', application, path], signal, 'hidden');
|
|
176
176
|
}
|
|
177
177
|
/** Resolve the desktop that owns the file, including Windows applications reached from WSL. */
|
|
178
178
|
async function desktopTarget(path, signal, internals) {
|
|
179
179
|
signal.throwIfAborted();
|
|
180
180
|
const platform = internals.platform ?? process.platform;
|
|
181
181
|
if (platform === 'linux' && nativeFileManager(internals) === 'explorer') {
|
|
182
|
-
const translated = await (internals.run ?? runNativeCommand)('wslpath', ['-w', path], signal);
|
|
182
|
+
const translated = await (internals.run ?? runNativeCommand)('wslpath', ['-w', path], signal, 'hidden');
|
|
183
183
|
signal.throwIfAborted();
|
|
184
184
|
const windowsPath = translated.stdout.replace(/[\r\n]+$/, '');
|
|
185
185
|
if (windowsPath === '')
|
package/lib/types/path-opener.js
CHANGED
|
@@ -38,7 +38,7 @@ async function openInBrowser(path, signal, platform, run, env) {
|
|
|
38
38
|
if (platform === 'darwin') {
|
|
39
39
|
let bundle;
|
|
40
40
|
try {
|
|
41
|
-
const { stdout } = await run('defaults', ['read', 'com.apple.LaunchServices/com.apple.launchservices.secure'], signal);
|
|
41
|
+
const { stdout } = await run('defaults', ['read', 'com.apple.LaunchServices/com.apple.launchservices.secure'], signal, 'hidden');
|
|
42
42
|
bundle = macBundleForHttps(stdout);
|
|
43
43
|
}
|
|
44
44
|
catch {
|
|
@@ -48,7 +48,7 @@ async function openInBrowser(path, signal, platform, run, env) {
|
|
|
48
48
|
}
|
|
49
49
|
if (bundle === undefined)
|
|
50
50
|
return false;
|
|
51
|
-
await run('open', ['-b', bundle, path], signal);
|
|
51
|
+
await run('open', ['-b', bundle, path], signal, 'hidden');
|
|
52
52
|
return true;
|
|
53
53
|
}
|
|
54
54
|
if (platform === 'linux') {
|
|
@@ -57,7 +57,7 @@ async function openInBrowser(path, signal, platform, run, env) {
|
|
|
57
57
|
const browser = env.BROWSER;
|
|
58
58
|
if (browser === undefined || browser === '')
|
|
59
59
|
return false;
|
|
60
|
-
await run(browser, [path], signal);
|
|
60
|
+
await run(browser, [path], signal, 'hidden');
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
63
|
// Windows names no browser without reading the UserChoice registry, and its
|
|
@@ -112,7 +112,8 @@ function explorerTarget(windowsPath) {
|
|
|
112
112
|
*/
|
|
113
113
|
async function runExplorer(args, signal, run) {
|
|
114
114
|
try {
|
|
115
|
-
|
|
115
|
+
// SW_HIDE also hides Explorer's folder window, not just console windows.
|
|
116
|
+
await run('explorer.exe', args, signal, 'visible');
|
|
116
117
|
}
|
|
117
118
|
catch (error) {
|
|
118
119
|
signal.throwIfAborted();
|
|
@@ -133,7 +134,7 @@ async function openWindowsPath(path, signal, run) {
|
|
|
133
134
|
}
|
|
134
135
|
/** Translate a WSL path before handing it to the Windows desktop. */
|
|
135
136
|
async function openWslPath(path, signal, run) {
|
|
136
|
-
const translated = await run('wslpath', ['-w', path], signal);
|
|
137
|
+
const translated = await run('wslpath', ['-w', path], signal, 'hidden');
|
|
137
138
|
signal.throwIfAborted();
|
|
138
139
|
const windowsPath = translated.stdout.replace(/[\r\n]+$/, '');
|
|
139
140
|
if (windowsPath === '')
|
|
@@ -150,7 +151,7 @@ async function openNativePathWithIntent(path, signal, intent, internals = {}) {
|
|
|
150
151
|
&& await openInBrowser(path, signal, platform, run, env))
|
|
151
152
|
return;
|
|
152
153
|
if (platform === 'darwin') {
|
|
153
|
-
await run('open', intent === 'text-editor' ? ['-t', path] : [path], signal);
|
|
154
|
+
await run('open', intent === 'text-editor' ? ['-t', path] : [path], signal, 'hidden');
|
|
154
155
|
return;
|
|
155
156
|
}
|
|
156
157
|
if (platform === 'win32') {
|
|
@@ -162,7 +163,7 @@ async function openNativePathWithIntent(path, signal, intent, internals = {}) {
|
|
|
162
163
|
await openWslPath(path, signal, run);
|
|
163
164
|
return;
|
|
164
165
|
}
|
|
165
|
-
await run('xdg-open', [path], signal);
|
|
166
|
+
await run('xdg-open', [path], signal, 'hidden');
|
|
166
167
|
return;
|
|
167
168
|
}
|
|
168
169
|
throw new Error(`native path opener is unsupported on ${platform}`);
|
|
@@ -243,13 +244,13 @@ export async function revealNativePath(path, signal, internals = {}) {
|
|
|
243
244
|
const run = internals.run ?? runNativeCommand;
|
|
244
245
|
const manager = nativeFileManager({ ...internals, platform });
|
|
245
246
|
if (manager === 'finder') {
|
|
246
|
-
await run('open', ['-R', path], signal);
|
|
247
|
+
await run('open', ['-R', path], signal, 'hidden');
|
|
247
248
|
return;
|
|
248
249
|
}
|
|
249
250
|
if (manager === 'explorer') {
|
|
250
251
|
let windowsPath = path;
|
|
251
252
|
if (platform === 'linux') {
|
|
252
|
-
const translated = await run('wslpath', ['-w', path], signal);
|
|
253
|
+
const translated = await run('wslpath', ['-w', path], signal, 'hidden');
|
|
253
254
|
signal.throwIfAborted();
|
|
254
255
|
windowsPath = translated.stdout.replace(/[\r\n]+$/, '');
|
|
255
256
|
if (windowsPath === '')
|
|
@@ -259,7 +260,7 @@ export async function revealNativePath(path, signal, internals = {}) {
|
|
|
259
260
|
return;
|
|
260
261
|
}
|
|
261
262
|
if (manager === 'directory') {
|
|
262
|
-
await run('xdg-open', [dirname(path)], signal);
|
|
263
|
+
await run('xdg-open', [dirname(path)], signal, 'hidden');
|
|
263
264
|
return;
|
|
264
265
|
}
|
|
265
266
|
throw new Error(`native file manager is unsupported on ${platform}`);
|
package/lib/types/runner.d.ts
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* @module @deepseek-ai/dsh-native-command/runner
|
|
4
4
|
*/
|
|
5
5
|
/** Testable command boundary; native implementations never invoke a shell. */
|
|
6
|
-
export type NativeCommandRunner = (command: string, args: readonly string[], signal: AbortSignal) => Promise<{
|
|
6
|
+
export type NativeCommandRunner = (command: string, args: readonly string[], signal: AbortSignal, window: 'hidden' | 'visible') => Promise<{
|
|
7
7
|
stdout: string;
|
|
8
8
|
stderr: string;
|
|
9
9
|
}>;
|
|
10
10
|
/**
|
|
11
|
-
* Run a host command with utf8 stdio, abort propagation, and
|
|
11
|
+
* Run a host command with utf8 stdio, abort propagation, and explicit GUI visibility.
|
|
12
12
|
* @param command - executable path or PATH name.
|
|
13
13
|
* @param args - argv (never a shell string).
|
|
14
14
|
* @param signal - caller/connection lifetime; abort terminates the child.
|
|
15
|
+
* @param window - Windows startup visibility: `hidden` for background commands,
|
|
16
|
+
* `visible` for GUI launchers. Ignored on other platforms.
|
|
15
17
|
* @returns captured stdout/stderr on exit 0.
|
|
16
18
|
*/
|
|
17
19
|
export declare const runNativeCommand: NativeCommandRunner;
|
package/lib/types/runner.js
CHANGED
|
@@ -4,14 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
/**
|
|
7
|
-
* Run a host command with utf8 stdio, abort propagation, and
|
|
7
|
+
* Run a host command with utf8 stdio, abort propagation, and explicit GUI visibility.
|
|
8
8
|
* @param command - executable path or PATH name.
|
|
9
9
|
* @param args - argv (never a shell string).
|
|
10
10
|
* @param signal - caller/connection lifetime; abort terminates the child.
|
|
11
|
+
* @param window - Windows startup visibility: `hidden` for background commands,
|
|
12
|
+
* `visible` for GUI launchers. Ignored on other platforms.
|
|
11
13
|
* @returns captured stdout/stderr on exit 0.
|
|
12
14
|
*/
|
|
13
|
-
export const runNativeCommand = (command, args, signal) => new Promise((resolve, reject) => {
|
|
14
|
-
execFile(command, [...args], { encoding: 'utf8', signal, windowsHide:
|
|
15
|
+
export const runNativeCommand = (command, args, signal, window) => new Promise((resolve, reject) => {
|
|
16
|
+
execFile(command, [...args], { encoding: 'utf8', signal, windowsHide: window === 'hidden' }, (error, stdout, stderr) => {
|
|
15
17
|
if (error !== null) {
|
|
16
18
|
const failure = Object.assign(new Error(error.message, { cause: error }), {
|
|
17
19
|
code: error.code,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-native-command",
|
|
3
3
|
"description": "Host-native command and path-opening utilities with shell-free execution, cancellation, desktop detection, and WSL handoff",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0-rc.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|