@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 CHANGED
@@ -1,6 +1,52 @@
1
- # Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
2
- # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
- # after editing either side, bring the other along and re-record with:
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
- README.md: c9e1f3fd936093bcad334390704b97630f7acfc8
6
- README.zh.md: 2c6feadbb8e09391ab5f112e0129b669f6c33127
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 hides transient Windows consoles. 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.
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; `windowsHide` suppresses the transient console window on Windows. 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`.
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 输出、传播取消,并隐藏 Windows 瞬时控制台。路径打开器支持默认应用与文本编辑器意图、浏览器可渲染文档、WSL 转换与桌面可用性检查。它是库而非插件:没有 `ctx`、无状态、不发事件。
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 关联处理器,Linux 使用 GIO,并共用 XDG 桌面文件和图标读取逻辑。macOS 上 bundle 标识符与显示名都相同的多份拷贝(自更新暂存副本、按版本安装的拷贝)合并为系统默认项,否则保留最高版本;显示名不同的并存安装两项都保留。`openNativeFileApplication(path, application, signal)` 按完整的当前注册列表重新验证关联应用,展示列表中被合并掉的拷贝仍可打开,且不修改系统默认值。Windows 交给 Shell 启动应用,Linux 交给 `gio launch` 展开参数。WSL 转换路径后使用 Windows 适配器。调用方负责验证本地文件路径。原生集成测试分别在相应平台使用独立的 Windows 文件关联和 Linux XDG 目录。
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` 选项在调用方中止触发时终止子进程;`windowsHide` 在 Windows 上抑制瞬时控制台窗口。遇到非零退出或 spawn 错误时,回调把 `code`、`stdout`、`stderr` 挂到被拒绝的错误上,并保留原始错误作为 `cause`。
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 Windows hide.
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: true
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
- static string IconData(IHandler handler) {
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
- return (await run("powershell.exe", [
655
- "-NoProfile",
656
- "-NonInteractive",
657
- "-STA",
658
- "-EncodedCommand",
659
- Buffer.from(script, "utf16le").toString("base64")
660
- ], signal)).stdout;
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
- static string IconData(IHandler handler) {
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
- const result = await run('powershell.exe', [
224
- '-NoProfile', '-NonInteractive', '-STA', '-EncodedCommand', Buffer.from(script, 'utf16le').toString('base64'),
225
- ], signal);
226
- return result.stdout;
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 === '')
@@ -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
- await run('explorer.exe', args, signal);
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}`);
@@ -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 Windows hide.
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;
@@ -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 Windows hide.
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: true }, (error, stdout, stderr) => {
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.1.7-rc.1",
4
+ "version": "0.2.0-rc.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },