@deepseek-ai/dsh-native-command 0.1.6-alpha.2 → 0.1.7-alpha.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 +2 -2
- package/README.md +7 -3
- package/README.zh.md +7 -3
- package/lib/index.js +612 -3
- package/lib/types/desktop-entry.d.ts +26 -0
- package/lib/types/desktop-entry.js +79 -0
- package/lib/types/file-applications-linux.d.ts +12 -0
- package/lib/types/file-applications-linux.js +70 -0
- package/lib/types/file-applications-windows.d.ts +12 -0
- package/lib/types/file-applications-windows.js +228 -0
- package/lib/types/file-applications.d.ts +22 -0
- package/lib/types/file-applications.js +191 -0
- package/lib/types/index.d.ts +4 -1
- package/lib/types/index.js +9 -0
- package/lib/types/path-opener.d.ts +8 -0
- package/lib/types/path-opener.js +230 -0
- package/lib/types/runner.js +27 -0
- package/lib/types/types.d.ts +18 -0
- package/lib/types/types.js +25 -0
- package/package.json +9 -4
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform native path and text-document openers for Host UI
|
|
3
|
+
* integrations.
|
|
4
|
+
*
|
|
5
|
+
* The default intent prefers the default browser for documents it renders when
|
|
6
|
+
* the platform can name one, then falls back to the default application. WSL
|
|
7
|
+
* translates every path for the Windows desktop instead of assuming a Linux
|
|
8
|
+
* GUI. The text-editor intent never consults the browser.
|
|
9
|
+
* @module @deepseek-ai/dsh-native-command/path-opener
|
|
10
|
+
*/
|
|
11
|
+
import { release as osRelease } from 'node:os';
|
|
12
|
+
import { dirname, extname } from 'node:path';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
|
+
import { runNativeCommand } from "./runner.js";
|
|
15
|
+
/** Documents a browser renders, as opposed to ones an editor merely edits. */
|
|
16
|
+
const BROWSER_DOCUMENTS = new Set(['.html', '.htm', '.xhtml', '.svg']);
|
|
17
|
+
/**
|
|
18
|
+
* The macOS bundle registered for `https` — the default browser, as
|
|
19
|
+
* LaunchServices records it. The nested version dict is stripped first
|
|
20
|
+
* because it carries its own `LSHandlerRoleAll`.
|
|
21
|
+
*/
|
|
22
|
+
function macBundleForHttps(plist) {
|
|
23
|
+
const stripped = plist.replace(/LSHandlerPreferredVersions\s*=\s*\{[^}]*\};/g, '');
|
|
24
|
+
const block = /\{[^{}]*LSHandlerURLScheme\s*=\s*"?https"?;[^{}]*\}/.exec(stripped)?.[0];
|
|
25
|
+
if (block === undefined)
|
|
26
|
+
return undefined;
|
|
27
|
+
return /LSHandlerRoleAll\s*=\s*"?([\w.-]+)"?;/.exec(block)?.[1];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Open one browser-renderable document with the default browser.
|
|
31
|
+
* @returns true when a browser took it; false when this platform cannot name
|
|
32
|
+
* one, or naming it failed — the caller then uses the default application.
|
|
33
|
+
*/
|
|
34
|
+
async function openInBrowser(path, signal, platform, run, env) {
|
|
35
|
+
if (platform === 'darwin') {
|
|
36
|
+
let bundle;
|
|
37
|
+
try {
|
|
38
|
+
const { stdout } = await run('defaults', ['read', 'com.apple.LaunchServices/com.apple.launchservices.secure'], signal);
|
|
39
|
+
bundle = macBundleForHttps(stdout);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// No LaunchServices record (a fresh account never changed a default):
|
|
43
|
+
// the content-type handler is then the system's own choice anyway.
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (bundle === undefined)
|
|
47
|
+
return false;
|
|
48
|
+
await run('open', ['-b', bundle, path], signal);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (platform === 'linux') {
|
|
52
|
+
// $BROWSER is the portable convention; desktop-entry resolution through
|
|
53
|
+
// xdg-settings needs a launcher this package has no business shipping.
|
|
54
|
+
const browser = env.BROWSER;
|
|
55
|
+
if (browser === undefined || browser === '')
|
|
56
|
+
return false;
|
|
57
|
+
await run(browser, [path], signal);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// Windows names no browser without reading the UserChoice registry, and its
|
|
61
|
+
// .html association is the browser in the ordinary case.
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
/** PowerShell single-quoted literal (doubles embedded quotes). */
|
|
65
|
+
function powershellLiteral(path) {
|
|
66
|
+
return `'${path.replace(/'/g, "''")}'`;
|
|
67
|
+
}
|
|
68
|
+
/** Whether one environment marker is set to a non-empty value. */
|
|
69
|
+
function present(value) {
|
|
70
|
+
return value !== undefined && value !== '';
|
|
71
|
+
}
|
|
72
|
+
/** Distinguish WSL from desktop Linux using its process and kernel markers. */
|
|
73
|
+
function isWsl(internals) {
|
|
74
|
+
const env = internals.env ?? process.env;
|
|
75
|
+
if (present(env.WSL_DISTRO_NAME) || present(env.WSL_INTEROP))
|
|
76
|
+
return true;
|
|
77
|
+
return (internals.osRelease ?? osRelease()).toLowerCase().includes('microsoft');
|
|
78
|
+
}
|
|
79
|
+
/** Open one Windows-resolvable path through its registered desktop application. */
|
|
80
|
+
async function openWindowsPath(path, signal, run) {
|
|
81
|
+
await run('powershell.exe', [
|
|
82
|
+
'-NoProfile',
|
|
83
|
+
'-Command',
|
|
84
|
+
`Invoke-Item -LiteralPath ${powershellLiteral(path)}`,
|
|
85
|
+
], signal);
|
|
86
|
+
}
|
|
87
|
+
/** Translate a WSL path before handing it to the Windows desktop. */
|
|
88
|
+
async function openWslPath(path, signal, run) {
|
|
89
|
+
const translated = await run('wslpath', ['-w', path], signal);
|
|
90
|
+
signal.throwIfAborted();
|
|
91
|
+
const windowsPath = translated.stdout.replace(/[\r\n]+$/, '');
|
|
92
|
+
if (windowsPath === '')
|
|
93
|
+
throw new Error('wslpath returned no Windows path');
|
|
94
|
+
await openWindowsPath(windowsPath, signal, run);
|
|
95
|
+
}
|
|
96
|
+
/** Dispatch one shell-free platform command for the requested open intent. */
|
|
97
|
+
async function openNativePathWithIntent(path, signal, intent, internals = {}) {
|
|
98
|
+
const platform = internals.platform ?? process.platform;
|
|
99
|
+
const run = internals.run ?? runNativeCommand;
|
|
100
|
+
const env = internals.env ?? process.env;
|
|
101
|
+
const wsl = platform === 'linux' && isWsl(internals);
|
|
102
|
+
if (!wsl && intent === 'default' && BROWSER_DOCUMENTS.has(extname(path).toLowerCase())
|
|
103
|
+
&& await openInBrowser(path, signal, platform, run, env))
|
|
104
|
+
return;
|
|
105
|
+
if (platform === 'darwin') {
|
|
106
|
+
await run('open', intent === 'text-editor' ? ['-t', path] : [path], signal);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (platform === 'win32') {
|
|
110
|
+
await openWindowsPath(path, signal, run);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (platform === 'linux') {
|
|
114
|
+
if (wsl) {
|
|
115
|
+
await openWslPath(path, signal, run);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
await run('xdg-open', [path], signal);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
throw new Error(`native path opener is unsupported on ${platform}`);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Whether {@link openNativePath} plausibly reaches a desktop on this host.
|
|
125
|
+
*
|
|
126
|
+
* macOS and Windows always carry a desktop opener; Linux does when it is WSL
|
|
127
|
+
* (the Windows desktop takes the path) or a display server is announced.
|
|
128
|
+
* A headless or containerised Linux host answers false, which is what lets a
|
|
129
|
+
* surface show a path as text instead of offering a button that would spawn
|
|
130
|
+
* `xdg-open` into nothing.
|
|
131
|
+
* @param internals - platform and environment seam for deterministic tests.
|
|
132
|
+
* @returns true when handing a path to the native opener can work at all.
|
|
133
|
+
*/
|
|
134
|
+
export function canOpenNativePath(internals = {}) {
|
|
135
|
+
const platform = internals.platform ?? process.platform;
|
|
136
|
+
if (platform === 'darwin' || platform === 'win32')
|
|
137
|
+
return true;
|
|
138
|
+
if (platform !== 'linux')
|
|
139
|
+
return false;
|
|
140
|
+
const env = internals.env ?? process.env;
|
|
141
|
+
return isWsl(internals) || present(env.DISPLAY) || present(env.WAYLAND_DISPLAY);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Open a filesystem path with the operating system's default application, or
|
|
145
|
+
* with the default browser when the path names a document a browser renders.
|
|
146
|
+
* @param path - absolute or host-resolvable path (caller owns resolution).
|
|
147
|
+
* @param signal - caller/connection lifetime; abort terminates the native command.
|
|
148
|
+
* @param internals - Platform, environment, and runner hooks for deterministic tests.
|
|
149
|
+
*/
|
|
150
|
+
export function openNativePath(path, signal, internals = {}) {
|
|
151
|
+
return openNativePathWithIntent(path, signal, 'default', internals);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Open a filesystem path through its file-type association, including HTML and SVG.
|
|
155
|
+
* @param path - absolute or host-resolvable path; the caller verifies local access.
|
|
156
|
+
* @param signal - caller lifetime; abort terminates the native command.
|
|
157
|
+
* @param internals - platform, environment, and runner facts for adapter tests.
|
|
158
|
+
* @returns after the associated application accepts the path.
|
|
159
|
+
*/
|
|
160
|
+
export function openNativeAssociatedPath(path, signal, internals = {}) {
|
|
161
|
+
return openNativePathWithIntent(path, signal, 'association', internals);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Open a text document for editing; macOS bypasses the file-type association
|
|
165
|
+
* so a YAML association with a browser cannot consume the gesture.
|
|
166
|
+
* @param path - absolute or host-resolvable text-document path.
|
|
167
|
+
* @param signal - caller/connection lifetime; abort terminates the native command.
|
|
168
|
+
* @param internals - Platform and runner hooks for deterministic tests.
|
|
169
|
+
*/
|
|
170
|
+
export function openNativeTextFile(path, signal, internals = {}) {
|
|
171
|
+
return openNativePathWithIntent(path, signal, 'text-editor', internals);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Identify the native file-manager action without inspecting the browser's platform.
|
|
175
|
+
* @param internals - platform and WSL facts.
|
|
176
|
+
* @returns the supported file-manager action, or null on unsupported platforms.
|
|
177
|
+
*/
|
|
178
|
+
export function nativeFileManager(internals = {}) {
|
|
179
|
+
const platform = internals.platform ?? process.platform;
|
|
180
|
+
if (platform === 'darwin')
|
|
181
|
+
return 'finder';
|
|
182
|
+
if (platform === 'win32' || (platform === 'linux' && isWsl(internals)))
|
|
183
|
+
return 'explorer';
|
|
184
|
+
return platform === 'linux' ? 'directory' : null;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Reveal a file in Finder or Explorer, or open its parent in the Linux default file manager.
|
|
188
|
+
* @param path - absolute file path already authorized by the caller.
|
|
189
|
+
* @param signal - caller lifetime; abort terminates the native command.
|
|
190
|
+
* @param internals - platform, environment, and command runner for adapter tests.
|
|
191
|
+
* @returns after command completion; Explorer exit 1 is accepted as a delegated handoff, not proof of selection.
|
|
192
|
+
*/
|
|
193
|
+
export async function revealNativePath(path, signal, internals = {}) {
|
|
194
|
+
signal.throwIfAborted();
|
|
195
|
+
const platform = internals.platform ?? process.platform;
|
|
196
|
+
const run = internals.run ?? runNativeCommand;
|
|
197
|
+
const manager = nativeFileManager({ ...internals, platform });
|
|
198
|
+
if (manager === 'finder') {
|
|
199
|
+
await run('open', ['-R', path], signal);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (manager === 'explorer') {
|
|
203
|
+
let windowsPath = path;
|
|
204
|
+
if (platform === 'linux') {
|
|
205
|
+
const translated = await run('wslpath', ['-w', path], signal);
|
|
206
|
+
signal.throwIfAborted();
|
|
207
|
+
windowsPath = translated.stdout.replace(/[\r\n]+$/, '');
|
|
208
|
+
if (windowsPath === '')
|
|
209
|
+
throw new Error('wslpath returned no Windows path');
|
|
210
|
+
}
|
|
211
|
+
// Explorer parses commas itself; a file URI preserves commas and whitespace in the path.
|
|
212
|
+
const target = pathToFileURL(windowsPath, { windows: true }).href.replaceAll(',', '%2C');
|
|
213
|
+
try {
|
|
214
|
+
await run('explorer.exe', ['/select,', target], signal);
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
signal.throwIfAborted();
|
|
218
|
+
// Explorer can exit 1 after delegating to the existing desktop process.
|
|
219
|
+
if (!(error instanceof Error) || !('code' in error) || error.code !== 1)
|
|
220
|
+
throw error;
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (manager === 'directory') {
|
|
225
|
+
await run('xdg-open', [dirname(path)], signal);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
throw new Error(`native file manager is unsupported on ${platform}`);
|
|
229
|
+
}
|
|
230
|
+
//# sourceMappingURL=path-opener.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared no-shell `execFile` runner for host-native OS integrations.
|
|
3
|
+
* @module @deepseek-ai/dsh-native-command/runner
|
|
4
|
+
*/
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
/**
|
|
7
|
+
* Run a host command with utf8 stdio, abort propagation, and Windows hide.
|
|
8
|
+
* @param command - executable path or PATH name.
|
|
9
|
+
* @param args - argv (never a shell string).
|
|
10
|
+
* @param signal - caller/connection lifetime; abort terminates the child.
|
|
11
|
+
* @returns captured stdout/stderr on exit 0.
|
|
12
|
+
*/
|
|
13
|
+
export const runNativeCommand = (command, args, signal) => new Promise((resolve, reject) => {
|
|
14
|
+
execFile(command, [...args], { encoding: 'utf8', signal, windowsHide: true }, (error, stdout, stderr) => {
|
|
15
|
+
if (error !== null) {
|
|
16
|
+
const failure = Object.assign(new Error(error.message, { cause: error }), {
|
|
17
|
+
code: error.code,
|
|
18
|
+
stdout,
|
|
19
|
+
stderr,
|
|
20
|
+
});
|
|
21
|
+
reject(failure);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
resolve({ stdout, stderr });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Browser-safe metadata for native file associations. */
|
|
2
|
+
/** One OS-registered application capable of opening the requested file. */
|
|
3
|
+
export interface NativeFileApplication {
|
|
4
|
+
/** OS application identifier; callers must revalidate it against the file's current handlers before opening. */
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly default: boolean;
|
|
8
|
+
/** PNG or SVG data URL, or null when the desktop supplies no icon. */
|
|
9
|
+
readonly icon: string | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Validate file association metadata received from a native process or authenticated Host.
|
|
13
|
+
* @param value - decoded application list.
|
|
14
|
+
* @returns validated application metadata.
|
|
15
|
+
* @throws Error for malformed entries or unsupported icon URLs.
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseNativeFileApplications(value: unknown): readonly NativeFileApplication[];
|
|
18
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Browser-safe metadata for native file associations. */
|
|
2
|
+
/**
|
|
3
|
+
* Validate file association metadata received from a native process or authenticated Host.
|
|
4
|
+
* @param value - decoded application list.
|
|
5
|
+
* @returns validated application metadata.
|
|
6
|
+
* @throws Error for malformed entries or unsupported icon URLs.
|
|
7
|
+
*/
|
|
8
|
+
export function parseNativeFileApplications(value) {
|
|
9
|
+
if (!Array.isArray(value))
|
|
10
|
+
throw new Error('Invalid native application list');
|
|
11
|
+
const applications = [];
|
|
12
|
+
const entries = value;
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (typeof entry !== 'object' || entry === null
|
|
15
|
+
|| !('id' in entry) || !('name' in entry) || !('default' in entry) || !('icon' in entry)
|
|
16
|
+
|| typeof entry.id !== 'string' || entry.id.length === 0
|
|
17
|
+
|| typeof entry.name !== 'string' || typeof entry.default !== 'boolean'
|
|
18
|
+
|| !(entry.icon === null || (typeof entry.icon === 'string' && /^data:image\/(?:png|svg\+xml);base64,[A-Za-z0-9+/=]+$/.test(entry.icon)))) {
|
|
19
|
+
throw new Error('Invalid native application entry');
|
|
20
|
+
}
|
|
21
|
+
applications.push({ id: entry.id, name: entry.name, default: entry.default, icon: entry.icon });
|
|
22
|
+
}
|
|
23
|
+
return applications;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=types.js.map
|
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.
|
|
4
|
+
"version": "0.1.7-alpha.1",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -19,17 +19,22 @@
|
|
|
19
19
|
"default": "./lib/index.js"
|
|
20
20
|
},
|
|
21
21
|
"./src/*": "./src/*",
|
|
22
|
-
"./package.json": "./package.json"
|
|
22
|
+
"./package.json": "./package.json",
|
|
23
|
+
"./types": {
|
|
24
|
+
"types": "./lib/types/types.d.ts",
|
|
25
|
+
"default": "./lib/types/types.js"
|
|
26
|
+
}
|
|
23
27
|
},
|
|
24
28
|
"files": [
|
|
25
29
|
"lib/index.js",
|
|
30
|
+
"lib/types/**/*.js",
|
|
26
31
|
"lib/types/**/*.d.ts"
|
|
27
32
|
],
|
|
28
33
|
"license": "MIT",
|
|
29
34
|
"peerDependencies": {
|
|
30
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
35
|
+
"@deepseek-ai/cordis": "^4.0.3"
|
|
31
36
|
},
|
|
32
37
|
"devDependencies": {
|
|
33
|
-
"@deepseek-ai/cordis": "^4.0.
|
|
38
|
+
"@deepseek-ai/cordis": "^4.0.3"
|
|
34
39
|
}
|
|
35
40
|
}
|