@flighthq/shell 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/shell.d.ts +10 -19
- package/dist/shell.d.ts.map +1 -1
- package/dist/shell.js +28 -130
- package/dist/shell.js.map +1 -1
- package/package.json +4 -3
- package/src/shell.test.ts +162 -315
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @flighthq/shell
|
|
2
2
|
|
|
3
|
-
OS shell
|
|
3
|
+
OS shell commands over six explicit Host capability slots with required external URL policy
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/shell`. The `@flight
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/shell@0.5.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/shell@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/shell)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { isShellUrlAllowed, moveShellItemToTrash, moveShellItemsToTrash, openShellExternalUrl, openShellPath, readShellShortcutLink, revealShellPath, shellBeep, writeShellShortcutLink, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,qBAAqB,EACrB,eAAe,EACf,SAAS,EACT,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { isShellUrlAllowed, moveShellItemToTrash, moveShellItemsToTrash, openShellExternalUrl, openShellPath, readShellShortcutLink, revealShellPath, shellBeep, writeShellShortcutLink, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,aAAa,EACb,qBAAqB,EACrB,eAAe,EACf,SAAS,EACT,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
|
package/dist/shell.d.ts
CHANGED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
7
|
-
export declare function
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function
|
|
10
|
-
export declare function
|
|
11
|
-
export declare function openShellPath(path: string, options?: Readonly<ShellOpenPathOptions>): Promise<boolean>;
|
|
12
|
-
export declare function openShellPathResult(path: string, options?: Readonly<ShellOpenPathOptions>): Promise<string>;
|
|
13
|
-
export declare function readShellShortcutLink(shortcutPath: string): Promise<ShellShortcutLink | null>;
|
|
14
|
-
export declare function resetShellBackendForTest(): void;
|
|
15
|
-
export declare function setShellBackend(backend: ShellBackend | null): void;
|
|
16
|
-
export declare function setShellUrlSchemeAllowlist(schemes: readonly string[] | null): void;
|
|
17
|
-
export declare function shellBeep(): void;
|
|
18
|
-
export declare function showItemInFolder(path: string): Promise<boolean>;
|
|
19
|
-
export declare function writeShellShortcutLink(shortcutPath: string, link: Readonly<ShellShortcutLink>, operation?: ShellShortcutWriteOperation): Promise<boolean>;
|
|
1
|
+
import type { HasShellBeep, HasShellExternal, HasShellPathOpen, HasShellPathReveal, HasShellShortcutLink, HasShellTrash, ShellExternalOutcome, ShellExternalUrlPolicy, ShellPathOpenOutcome, ShellPathRevealOutcome, ShellShortcutLink, ShellShortcutLinkReadOutcome, ShellShortcutLinkWriteOutcome, ShellShortcutWriteOperation, ShellTrashOutcome } from '@flighthq/types/contract';
|
|
2
|
+
export declare function isShellUrlAllowed(url: string, policy: Readonly<ShellExternalUrlPolicy>): boolean;
|
|
3
|
+
export declare function moveShellItemsToTrash(host: HasShellTrash, paths: readonly string[]): Promise<readonly ShellTrashOutcome[]>;
|
|
4
|
+
export declare function moveShellItemToTrash(host: HasShellTrash, path: string): Promise<ShellTrashOutcome>;
|
|
5
|
+
export declare function openShellExternalUrl(host: HasShellExternal, url: string, policy: Readonly<ShellExternalUrlPolicy>): Promise<ShellExternalOutcome>;
|
|
6
|
+
export declare function openShellPath(host: HasShellPathOpen, path: string): Promise<ShellPathOpenOutcome>;
|
|
7
|
+
export declare function readShellShortcutLink(host: HasShellShortcutLink, shortcutPath: string): Promise<ShellShortcutLinkReadOutcome>;
|
|
8
|
+
export declare function revealShellPath(host: HasShellPathReveal, path: string): Promise<ShellPathRevealOutcome>;
|
|
9
|
+
export declare function shellBeep(host: HasShellBeep): void;
|
|
10
|
+
export declare function writeShellShortcutLink(host: HasShellShortcutLink, shortcutPath: string, link: Readonly<ShellShortcutLink>, operation: ShellShortcutWriteOperation): Promise<ShellShortcutLinkWriteOutcome>;
|
|
20
11
|
//# sourceMappingURL=shell.d.ts.map
|
package/dist/shell.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"shell.d.ts","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,oBAAoB,EACpB,sBAAsB,EACtB,oBAAoB,EACpB,sBAAsB,EACtB,iBAAiB,EACjB,4BAA4B,EAC5B,6BAA6B,EAC7B,2BAA2B,EAC3B,iBAAiB,EAClB,MAAM,0BAA0B,CAAC;AAIlC,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GAAG,OAAO,CAOhG;AAID,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,aAAa,EACnB,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,OAAO,CAAC,SAAS,iBAAiB,EAAE,CAAC,CAEvC;AAED,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAElG;AAKD,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,gBAAgB,EACtB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,QAAQ,CAAC,sBAAsB,CAAC,GACvC,OAAO,CAAC,oBAAoB,CAAC,CAG/B;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAEjG;AAED,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,oBAAoB,EAC1B,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,4BAA4B,CAAC,CAEvC;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAEvG;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAElD;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,oBAAoB,EAC1B,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,QAAQ,CAAC,iBAAiB,CAAC,EACjC,SAAS,EAAE,2BAA2B,GACrC,OAAO,CAAC,6BAA6B,CAAC,CAExC"}
|
package/dist/shell.js
CHANGED
|
@@ -1,145 +1,43 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
}
|
|
5
|
-
if (_host !== null) {
|
|
6
|
-
return {
|
|
7
|
-
conflict: _hostConflict,
|
|
8
|
-
layer: 'host',
|
|
9
|
-
operation: _hostObservation !== null ? _hostObservation.operation : null,
|
|
10
|
-
viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
|
-
return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
|
|
14
|
-
}
|
|
15
|
-
// The active shell backend. Precedence: custom > host > sentinel.
|
|
16
|
-
export function getShellBackend() {
|
|
17
|
-
return _custom ?? _host ?? _sentinel;
|
|
18
|
-
}
|
|
19
|
-
export function installShellHostBackend(backend) {
|
|
20
|
-
if (_host !== null) {
|
|
21
|
-
if (_host !== backend)
|
|
22
|
-
_hostConflict = true;
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
_host = backend;
|
|
26
|
-
}
|
|
27
|
-
// True when url is allowed by the active URL-scheme allowlist. When no allowlist is set, all URLs
|
|
28
|
-
// are allowed. Used internally by openShellExternalUrl; also exported for callers that need the check.
|
|
29
|
-
export function isShellUrlAllowed(url) {
|
|
30
|
-
if (_urlSchemeAllowlist === null)
|
|
31
|
-
return true;
|
|
1
|
+
// Pure policy validation used by openShellExternalUrl before any host effect. Callers must supply the
|
|
2
|
+
// policy on every invocation; an empty allowedSchemes array intentionally blocks every URL.
|
|
3
|
+
export function isShellUrlAllowed(url, policy) {
|
|
32
4
|
try {
|
|
33
|
-
const scheme = new URL(url).protocol.replace(/:$/, '');
|
|
34
|
-
return
|
|
5
|
+
const scheme = new URL(url).protocol.replace(/:$/, '').toLowerCase();
|
|
6
|
+
return policy.allowedSchemes.some((allowed) => allowed.toLowerCase() === scheme);
|
|
35
7
|
}
|
|
36
8
|
catch {
|
|
37
9
|
return false;
|
|
38
10
|
}
|
|
39
11
|
}
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
export function
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
// Moves a local path to the OS trash. Returns false on the web; native-host capability.
|
|
46
|
-
export function moveItemToTrash(path) {
|
|
47
|
-
return getShellBackend().moveToTrash(path);
|
|
48
|
-
}
|
|
49
|
-
export function observeShellHostResult(operation, succeeded) {
|
|
50
|
-
_hostObservation = {
|
|
51
|
-
operation,
|
|
52
|
-
viability: succeeded ? 'available' : 'runtime-api-unavailable',
|
|
53
|
-
};
|
|
12
|
+
// Projects the one-path provider operation across the batch. Promise.all starts every operation,
|
|
13
|
+
// awaits every settlement, and returns outcomes in the same order as paths.
|
|
14
|
+
export function moveShellItemsToTrash(host, paths) {
|
|
15
|
+
return Promise.all(paths.map((path) => host.shell.trash.moveToTrash(path)));
|
|
54
16
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
// SECURITY: handing an attacker-controlled URL to the OS default handler is the classic openExternal
|
|
58
|
-
// footgun — a non-http(s) scheme can launch a local application or protocol handler. When the url may
|
|
59
|
-
// be untrusted, constrain the accepted schemes with setShellUrlSchemeAllowlist (e.g. ['https',
|
|
60
|
-
// 'mailto']); isShellUrlAllowed exposes the same check for a caller that wants to gate the URL itself.
|
|
61
|
-
export function openShellExternalUrl(url, options) {
|
|
62
|
-
if (!isShellUrlAllowed(url))
|
|
63
|
-
return Promise.resolve(false);
|
|
64
|
-
return getShellBackend().openExternal(url, options);
|
|
17
|
+
export function moveShellItemToTrash(host, path) {
|
|
18
|
+
return host.shell.trash.moveToTrash(path);
|
|
65
19
|
}
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
20
|
+
// Handing a URL to an OS handler can launch a local application or registered protocol. The required
|
|
21
|
+
// per-call policy is validated before dispatch, so a blocked or malformed scheme never reaches the
|
|
22
|
+
// host. There is intentionally no policy default, ambient allowlist, or allow-all path.
|
|
23
|
+
export function openShellExternalUrl(host, url, policy) {
|
|
24
|
+
if (!isShellUrlAllowed(url, policy))
|
|
25
|
+
return Promise.resolve({ reason: 'blocked-scheme' });
|
|
26
|
+
return host.shell.external.open(url);
|
|
69
27
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// just a boolean; openShellPath is the boolean convenience wrapper over this.
|
|
73
|
-
export function openShellPathResult(path, options) {
|
|
74
|
-
return getShellBackend().openPathResult(path, options);
|
|
28
|
+
export function openShellPath(host, path) {
|
|
29
|
+
return host.shell.pathOpen.open(path);
|
|
75
30
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
export function readShellShortcutLink(shortcutPath) {
|
|
79
|
-
return getShellBackend().readShortcutLink(shortcutPath);
|
|
31
|
+
export function readShellShortcutLink(host, shortcutPath) {
|
|
32
|
+
return host.shell.shortcutLink.read(shortcutPath);
|
|
80
33
|
}
|
|
81
|
-
export function
|
|
82
|
-
|
|
83
|
-
_host = null;
|
|
84
|
-
_hostConflict = false;
|
|
85
|
-
_hostObservation = null;
|
|
34
|
+
export function revealShellPath(host, path) {
|
|
35
|
+
return host.shell.pathReveal.reveal(path);
|
|
86
36
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
_custom = backend;
|
|
37
|
+
export function shellBeep(host) {
|
|
38
|
+
host.shell.beep.beep();
|
|
90
39
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// scheme is not in the list. Example: setShellUrlSchemeAllowlist(['https', 'mailto']).
|
|
94
|
-
// This closes the classic openExternal security footgun with attacker-controlled URLs.
|
|
95
|
-
export function setShellUrlSchemeAllowlist(schemes) {
|
|
96
|
-
_urlSchemeAllowlist = schemes;
|
|
40
|
+
export function writeShellShortcutLink(host, shortcutPath, link, operation) {
|
|
41
|
+
return host.shell.shortcutLink.write(shortcutPath, link, operation);
|
|
97
42
|
}
|
|
98
|
-
// Emits a system beep. No-op on the web until a native host provides one.
|
|
99
|
-
export function shellBeep() {
|
|
100
|
-
getShellBackend().beep();
|
|
101
|
-
}
|
|
102
|
-
// Reveals a local path in the OS file manager. Returns false on the web; native-host capability.
|
|
103
|
-
export function showItemInFolder(path) {
|
|
104
|
-
return getShellBackend().showItemInFolder(path);
|
|
105
|
-
}
|
|
106
|
-
// Creates a Windows .lnk shell shortcut at shortcutPath pointing to link. Returns false on
|
|
107
|
-
// non-Windows platforms and on the web. operation defaults to 'create'.
|
|
108
|
-
// IMPORTANT: URL safety — only use openShellExternalUrl for user-facing link opening; do not pass
|
|
109
|
-
// attacker-controlled paths to writeShellShortcutLink without validation.
|
|
110
|
-
export function writeShellShortcutLink(shortcutPath, link, operation) {
|
|
111
|
-
return getShellBackend().writeShortcutLink(shortcutPath, link, operation);
|
|
112
|
-
}
|
|
113
|
-
const _sentinel = {
|
|
114
|
-
beep() { },
|
|
115
|
-
async moveItemsToTrash() {
|
|
116
|
-
return [];
|
|
117
|
-
},
|
|
118
|
-
async moveToTrash() {
|
|
119
|
-
return false;
|
|
120
|
-
},
|
|
121
|
-
async openExternal() {
|
|
122
|
-
return false;
|
|
123
|
-
},
|
|
124
|
-
async openPath() {
|
|
125
|
-
return false;
|
|
126
|
-
},
|
|
127
|
-
async openPathResult() {
|
|
128
|
-
return 'unavailable on web';
|
|
129
|
-
},
|
|
130
|
-
async readShortcutLink() {
|
|
131
|
-
return null;
|
|
132
|
-
},
|
|
133
|
-
async showItemInFolder() {
|
|
134
|
-
return false;
|
|
135
|
-
},
|
|
136
|
-
async writeShortcutLink() {
|
|
137
|
-
return false;
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
let _custom = null;
|
|
141
|
-
let _host = null;
|
|
142
|
-
let _hostConflict = false;
|
|
143
|
-
let _hostObservation = null;
|
|
144
|
-
let _urlSchemeAllowlist = null;
|
|
145
43
|
//# sourceMappingURL=shell.js.map
|
package/dist/shell.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"shell.js","sourceRoot":"","sources":["../src/shell.ts"],"names":[],"mappings":"AAkBA,sGAAsG;AACtG,4FAA4F;AAC5F,MAAM,UAAU,iBAAiB,CAAC,GAAW,EAAE,MAAwC;IACrF,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;QACrE,OAAO,MAAM,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,CAAC;IACnF,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,iGAAiG;AACjG,4EAA4E;AAC5E,MAAM,UAAU,qBAAqB,CACnC,IAAmB,EACnB,KAAwB;IAExB,OAAO,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAC9E,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAC,IAAmB,EAAE,IAAY;IACpE,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED,qGAAqG;AACrG,mGAAmG;AACnG,wFAAwF;AACxF,MAAM,UAAU,oBAAoB,CAClC,IAAsB,EACtB,GAAW,EACX,MAAwC;IAExC,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,MAAM,CAAC;QAAE,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC1F,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,IAAsB,EAAE,IAAY;IAChE,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,qBAAqB,CACnC,IAA0B,EAC1B,YAAoB;IAEpB,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AACpD,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,IAAwB,EAAE,IAAY;IACpE,OAAO,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAkB;IAC1C,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,sBAAsB,CACpC,IAA0B,EAC1B,YAAoB,EACpB,IAAiC,EACjC,SAAsC;IAEtC,OAAO,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,KAAK,CAAC,YAAY,EAAE,IAAI,EAAE,SAAS,CAAC,CAAC;AACtE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/shell",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1-edge.638.d4dbd4e",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,11 +38,12 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/types": "0.5.
|
|
41
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
+
"@flighthq/entity": "*",
|
|
44
45
|
"typescript": "^5.3.0"
|
|
45
46
|
},
|
|
46
|
-
"description": "OS shell
|
|
47
|
+
"description": "OS shell commands over six explicit Host capability slots with required external URL policy",
|
|
47
48
|
"sideEffects": false
|
|
48
49
|
}
|
package/src/shell.test.ts
CHANGED
|
@@ -1,359 +1,206 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createEntity } from '@flighthq/entity/contract';
|
|
2
|
+
import type {
|
|
3
|
+
EntityRuntimeKey,
|
|
4
|
+
HasShellBeep,
|
|
5
|
+
HasShellExternal,
|
|
6
|
+
HasShellPathOpen,
|
|
7
|
+
HasShellPathReveal,
|
|
8
|
+
HasShellShortcutLink,
|
|
9
|
+
HasShellTrash,
|
|
10
|
+
ShellBeepBackend,
|
|
11
|
+
ShellExternalBackend,
|
|
12
|
+
ShellExternalUrlPolicy,
|
|
13
|
+
ShellPathOpenBackend,
|
|
14
|
+
ShellPathRevealBackend,
|
|
15
|
+
ShellShortcutLinkBackend,
|
|
16
|
+
ShellTrashBackend,
|
|
17
|
+
} from '@flighthq/types/contract';
|
|
2
18
|
|
|
3
19
|
import {
|
|
4
|
-
explainShellBackend,
|
|
5
|
-
getShellBackend,
|
|
6
|
-
installShellHostBackend,
|
|
7
20
|
isShellUrlAllowed,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
observeShellHostResult,
|
|
21
|
+
moveShellItemsToTrash,
|
|
22
|
+
moveShellItemToTrash,
|
|
11
23
|
openShellExternalUrl,
|
|
12
24
|
openShellPath,
|
|
13
|
-
openShellPathResult,
|
|
14
25
|
readShellShortcutLink,
|
|
15
|
-
|
|
16
|
-
setShellBackend,
|
|
17
|
-
setShellUrlSchemeAllowlist,
|
|
26
|
+
revealShellPath,
|
|
18
27
|
shellBeep,
|
|
19
|
-
showItemInFolder,
|
|
20
28
|
writeShellShortcutLink,
|
|
21
29
|
} from './shell';
|
|
22
30
|
|
|
23
|
-
function fakeBackend(): ShellBackend & {
|
|
24
|
-
beeped: number;
|
|
25
|
-
lastOptions: ShellOpenExternalOptions | ShellOpenPathOptions | undefined;
|
|
26
|
-
opened: string;
|
|
27
|
-
pathResult: string;
|
|
28
|
-
trashed: string;
|
|
29
|
-
trashedBatch: readonly string[];
|
|
30
|
-
} {
|
|
31
|
-
return {
|
|
32
|
-
beeped: 0,
|
|
33
|
-
lastOptions: undefined,
|
|
34
|
-
opened: '',
|
|
35
|
-
pathResult: '',
|
|
36
|
-
trashed: '',
|
|
37
|
-
trashedBatch: [],
|
|
38
|
-
beep() {
|
|
39
|
-
this.beeped += 1;
|
|
40
|
-
},
|
|
41
|
-
async moveItemsToTrash(paths) {
|
|
42
|
-
this.trashedBatch = paths;
|
|
43
|
-
return paths.map(() => true);
|
|
44
|
-
},
|
|
45
|
-
async moveToTrash(path) {
|
|
46
|
-
this.trashed = path;
|
|
47
|
-
return true;
|
|
48
|
-
},
|
|
49
|
-
async openExternal(url, options) {
|
|
50
|
-
this.opened = url;
|
|
51
|
-
this.lastOptions = options;
|
|
52
|
-
return true;
|
|
53
|
-
},
|
|
54
|
-
async openPath(path, options) {
|
|
55
|
-
this.opened = path;
|
|
56
|
-
this.lastOptions = options;
|
|
57
|
-
return true;
|
|
58
|
-
},
|
|
59
|
-
async openPathResult(path, options) {
|
|
60
|
-
this.opened = path;
|
|
61
|
-
this.lastOptions = options;
|
|
62
|
-
return this.pathResult;
|
|
63
|
-
},
|
|
64
|
-
async readShortcutLink() {
|
|
65
|
-
return { target: '/path/to/target' };
|
|
66
|
-
},
|
|
67
|
-
async showItemInFolder(path) {
|
|
68
|
-
this.opened = path;
|
|
69
|
-
return true;
|
|
70
|
-
},
|
|
71
|
-
async writeShortcutLink() {
|
|
72
|
-
return true;
|
|
73
|
-
},
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function resetShellTestState(): void {
|
|
78
|
-
resetShellBackendForTest();
|
|
79
|
-
setShellUrlSchemeAllowlist(null);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// The root fast suite deliberately shares modules between files. A host-adapter test can therefore
|
|
83
|
-
// be the state immediately before this file; reset on entry as well as exit so the first assertion has
|
|
84
|
-
// the same isolation as every later one.
|
|
85
|
-
beforeEach(resetShellTestState);
|
|
86
|
-
afterEach(resetShellTestState);
|
|
87
|
-
|
|
88
|
-
describe('explainShellBackend', () => {
|
|
89
|
-
it('returns host-not-enabled when no backend is installed', () => {
|
|
90
|
-
expect(explainShellBackend()).toEqual({
|
|
91
|
-
conflict: false,
|
|
92
|
-
layer: 'host-not-enabled',
|
|
93
|
-
operation: null,
|
|
94
|
-
viability: 'unobserved',
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('returns custom layer when a custom backend is set', () => {
|
|
99
|
-
setShellBackend(fakeBackend());
|
|
100
|
-
expect(explainShellBackend().layer).toBe('custom');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('returns host layer when a host backend is installed', () => {
|
|
104
|
-
installShellHostBackend(fakeBackend());
|
|
105
|
-
expect(explainShellBackend().layer).toBe('host');
|
|
106
|
-
});
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe('getShellBackend', () => {
|
|
110
|
-
it('falls back to sentinel', () => {
|
|
111
|
-
expect(getShellBackend()).not.toBeNull();
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('returns the registered backend', () => {
|
|
115
|
-
const backend = fakeBackend();
|
|
116
|
-
setShellBackend(backend);
|
|
117
|
-
expect(getShellBackend()).toBe(backend);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
describe('installShellHostBackend', () => {
|
|
122
|
-
it('first-host-wins and sets conflict on second different backend', () => {
|
|
123
|
-
const first = fakeBackend();
|
|
124
|
-
const second = fakeBackend();
|
|
125
|
-
installShellHostBackend(first);
|
|
126
|
-
installShellHostBackend(second);
|
|
127
|
-
expect(explainShellBackend().conflict).toBe(true);
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('does not set conflict when the same backend is installed twice', () => {
|
|
131
|
-
const backend = fakeBackend();
|
|
132
|
-
installShellHostBackend(backend);
|
|
133
|
-
installShellHostBackend(backend);
|
|
134
|
-
expect(explainShellBackend().conflict).toBe(false);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
31
|
describe('isShellUrlAllowed', () => {
|
|
139
|
-
it('
|
|
140
|
-
expect(isShellUrlAllowed('https://example.
|
|
141
|
-
expect(isShellUrlAllowed('
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('
|
|
145
|
-
|
|
146
|
-
expect(isShellUrlAllowed('https://example.
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
const backend = fakeBackend();
|
|
175
|
-
setShellBackend(backend);
|
|
176
|
-
expect(await moveItemToTrash('/tmp/x')).toBe(true);
|
|
177
|
-
expect(backend.trashed).toBe('/tmp/x');
|
|
32
|
+
it('matches schemes case-insensitively from an explicit policy', () => {
|
|
33
|
+
expect(isShellUrlAllowed('https://example.test', { allowedSchemes: ['HTTPS'] })).toBe(true);
|
|
34
|
+
expect(isShellUrlAllowed('mailto:user@example.test', { allowedSchemes: ['https'] })).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('blocks malformed URLs and an empty policy', () => {
|
|
38
|
+
expect(isShellUrlAllowed('not a URL', { allowedSchemes: ['https'] })).toBe(false);
|
|
39
|
+
expect(isShellUrlAllowed('https://example.test', { allowedSchemes: [] })).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('moveShellItemsToTrash', () => {
|
|
44
|
+
it('starts every one-path operation and preserves input order while awaiting all results', async () => {
|
|
45
|
+
const paths: string[] = [];
|
|
46
|
+
const resolvers: Array<(outcome: { reason: 'ok' | 'operation-failed' }) => void> = [];
|
|
47
|
+
const host = trashHost(
|
|
48
|
+
createEntity({
|
|
49
|
+
moveToTrash(path) {
|
|
50
|
+
paths.push(path);
|
|
51
|
+
return new Promise((resolve) => resolvers.push(resolve));
|
|
52
|
+
},
|
|
53
|
+
} satisfies Omit<ShellTrashBackend, typeof EntityRuntimeKey>),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const outcomes = moveShellItemsToTrash(host, ['/first', '/second']);
|
|
57
|
+
expect(paths).toEqual(['/first', '/second']);
|
|
58
|
+
resolvers[1]?.({ reason: 'operation-failed' });
|
|
59
|
+
let settled = false;
|
|
60
|
+
void outcomes.then(() => {
|
|
61
|
+
settled = true;
|
|
62
|
+
});
|
|
63
|
+
await Promise.resolve();
|
|
64
|
+
expect(settled).toBe(false);
|
|
65
|
+
resolvers[0]?.({ reason: 'ok' });
|
|
66
|
+
await expect(outcomes).resolves.toEqual([{ reason: 'ok' }, { reason: 'operation-failed' }]);
|
|
178
67
|
});
|
|
179
68
|
});
|
|
180
69
|
|
|
181
|
-
describe('
|
|
182
|
-
it('
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
expect(
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
it('records a failed observation into the explanation', () => {
|
|
191
|
-
installShellHostBackend(fakeBackend());
|
|
192
|
-
observeShellHostResult('openExternal', false);
|
|
193
|
-
const explanation = explainShellBackend();
|
|
194
|
-
expect(explanation.operation).toBe('openExternal');
|
|
195
|
-
expect(explanation.viability).toBe('runtime-api-unavailable');
|
|
70
|
+
describe('moveShellItemToTrash', () => {
|
|
71
|
+
it('routes through the supplied trash provider', async () => {
|
|
72
|
+
const moveToTrash = vi.fn(async () => ({ reason: 'ok' as const }));
|
|
73
|
+
await expect(moveShellItemToTrash(trashHost(createEntity({ moveToTrash })), '/item')).resolves.toEqual({
|
|
74
|
+
reason: 'ok',
|
|
75
|
+
});
|
|
76
|
+
expect(moveToTrash).toHaveBeenCalledWith('/item');
|
|
196
77
|
});
|
|
197
78
|
});
|
|
198
79
|
|
|
199
80
|
describe('openShellExternalUrl', () => {
|
|
200
|
-
it('
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
expect(backend.opened).toBe('https://example.com');
|
|
81
|
+
it('requires policy as the third parameter at the type boundary', () => {
|
|
82
|
+
expectTypeOf(openShellExternalUrl).parameters.toEqualTypeOf<
|
|
83
|
+
[HasShellExternal, string, Readonly<ShellExternalUrlPolicy>]
|
|
84
|
+
>();
|
|
205
85
|
});
|
|
206
86
|
|
|
207
|
-
it('
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
await openShellExternalUrl('
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
87
|
+
it('blocks before dispatch when the scheme is not allowed', async () => {
|
|
88
|
+
const open = vi.fn(async () => ({ reason: 'ok' as const }));
|
|
89
|
+
const host = externalHost(createEntity({ open }));
|
|
90
|
+
await expect(openShellExternalUrl(host, 'file:///etc/passwd', { allowedSchemes: ['https'] })).resolves.toEqual({
|
|
91
|
+
reason: 'blocked-scheme',
|
|
92
|
+
});
|
|
93
|
+
expect(open).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('keeps two live hosts isolated', async () => {
|
|
97
|
+
const calls: string[] = [];
|
|
98
|
+
const first = externalHost(
|
|
99
|
+
createEntity({
|
|
100
|
+
async open(url) {
|
|
101
|
+
calls.push(`first:${url}`);
|
|
102
|
+
return { reason: 'ok' };
|
|
103
|
+
},
|
|
104
|
+
} satisfies Omit<ShellExternalBackend, typeof EntityRuntimeKey>),
|
|
105
|
+
);
|
|
106
|
+
const second = externalHost(
|
|
107
|
+
createEntity({
|
|
108
|
+
async open(url) {
|
|
109
|
+
calls.push(`second:${url}`);
|
|
110
|
+
return { reason: 'operation-failed' };
|
|
111
|
+
},
|
|
112
|
+
} satisfies Omit<ShellExternalBackend, typeof EntityRuntimeKey>),
|
|
113
|
+
);
|
|
114
|
+
await expect(openShellExternalUrl(first, 'https://one.test', HTTPS_ONLY)).resolves.toEqual({ reason: 'ok' });
|
|
115
|
+
await expect(openShellExternalUrl(second, 'https://two.test', HTTPS_ONLY)).resolves.toEqual({
|
|
116
|
+
reason: 'operation-failed',
|
|
117
|
+
});
|
|
118
|
+
expect(calls).toEqual(['first:https://one.test', 'second:https://two.test']);
|
|
228
119
|
});
|
|
229
120
|
});
|
|
230
121
|
|
|
231
122
|
describe('openShellPath', () => {
|
|
232
|
-
it('
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
await openShellPath('/tmp/x', { workingDirectory: '/home/user' });
|
|
243
|
-
expect(backend.lastOptions).toEqual({ workingDirectory: '/home/user' });
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('forwards the application option to the backend', async () => {
|
|
247
|
-
const backend = fakeBackend();
|
|
248
|
-
setShellBackend(backend);
|
|
249
|
-
await openShellPath('/tmp/x', { application: 'TextEdit' });
|
|
250
|
-
expect(backend.lastOptions).toEqual({ application: 'TextEdit' });
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('omits options when none provided', async () => {
|
|
254
|
-
const backend = fakeBackend();
|
|
255
|
-
setShellBackend(backend);
|
|
256
|
-
await openShellPath('/tmp/x');
|
|
257
|
-
expect(backend.lastOptions).toBeUndefined();
|
|
258
|
-
});
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
describe('openShellPathResult', () => {
|
|
262
|
-
it('returns an empty string on success', async () => {
|
|
263
|
-
const backend = fakeBackend();
|
|
264
|
-
backend.pathResult = '';
|
|
265
|
-
setShellBackend(backend);
|
|
266
|
-
expect(await openShellPathResult('/tmp/x')).toBe('');
|
|
267
|
-
expect(backend.opened).toBe('/tmp/x');
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('returns the OS error string on failure', async () => {
|
|
271
|
-
const backend = fakeBackend();
|
|
272
|
-
backend.pathResult = 'No such file or directory';
|
|
273
|
-
setShellBackend(backend);
|
|
274
|
-
expect(await openShellPathResult('/nonexistent')).toBe('No such file or directory');
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('forwards options to the backend', async () => {
|
|
278
|
-
const backend = fakeBackend();
|
|
279
|
-
setShellBackend(backend);
|
|
280
|
-
await openShellPathResult('/tmp/x', { workingDirectory: '/home/user' });
|
|
281
|
-
expect(backend.lastOptions).toEqual({ workingDirectory: '/home/user' });
|
|
123
|
+
it('preserves a provider failure and its message', async () => {
|
|
124
|
+
const provider = createEntity({
|
|
125
|
+
async open() {
|
|
126
|
+
return { message: '', reason: 'operation-failed' as const };
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
await expect(openShellPath(pathOpenHost(provider), '/missing')).resolves.toEqual({
|
|
130
|
+
message: '',
|
|
131
|
+
reason: 'operation-failed',
|
|
132
|
+
});
|
|
282
133
|
});
|
|
283
134
|
});
|
|
284
135
|
|
|
285
136
|
describe('readShellShortcutLink', () => {
|
|
286
|
-
it('returns
|
|
287
|
-
const
|
|
288
|
-
expect(
|
|
137
|
+
it('returns the read-specific outcome', async () => {
|
|
138
|
+
const provider = shortcutLinkProvider();
|
|
139
|
+
await expect(readShellShortcutLink(shortcutLinkHost(provider), '/app.lnk')).resolves.toEqual({
|
|
140
|
+
link: { target: '/app' },
|
|
141
|
+
reason: 'ok',
|
|
142
|
+
});
|
|
289
143
|
});
|
|
144
|
+
});
|
|
290
145
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
146
|
+
describe('revealShellPath', () => {
|
|
147
|
+
it('returns the reveal-specific outcome', async () => {
|
|
148
|
+
const reveal = vi.fn(async () => ({ reason: 'operation-failed' as const }));
|
|
149
|
+
await expect(revealShellPath(pathRevealHost(createEntity({ reveal })), '/item')).resolves.toEqual({
|
|
150
|
+
reason: 'operation-failed',
|
|
151
|
+
});
|
|
296
152
|
});
|
|
297
153
|
});
|
|
298
154
|
|
|
299
|
-
describe('
|
|
300
|
-
it('
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
expect(explainShellBackend().layer).toBe('host-not-enabled');
|
|
155
|
+
describe('shellBeep', () => {
|
|
156
|
+
it('dispatches synchronously', () => {
|
|
157
|
+
const beep = vi.fn();
|
|
158
|
+
shellBeep(beepHost(createEntity({ beep })));
|
|
159
|
+
expect(beep).toHaveBeenCalledOnce();
|
|
305
160
|
});
|
|
306
161
|
});
|
|
307
162
|
|
|
308
|
-
describe('
|
|
309
|
-
it('
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
163
|
+
describe('writeShellShortcutLink', () => {
|
|
164
|
+
it('requires an explicit write operation and returns the write-specific outcome', async () => {
|
|
165
|
+
const provider = shortcutLinkProvider();
|
|
166
|
+
await expect(
|
|
167
|
+
writeShellShortcutLink(shortcutLinkHost(provider), '/app.lnk', { target: '/app' }, 'replace'),
|
|
168
|
+
).resolves.toEqual({ reason: 'ok' });
|
|
169
|
+
expect(provider.write).toHaveBeenCalledWith('/app.lnk', { target: '/app' }, 'replace');
|
|
313
170
|
});
|
|
314
171
|
});
|
|
315
172
|
|
|
316
|
-
|
|
317
|
-
it('allows all URLs when set to null', () => {
|
|
318
|
-
setShellUrlSchemeAllowlist(['https']);
|
|
319
|
-
setShellUrlSchemeAllowlist(null);
|
|
320
|
-
expect(isShellUrlAllowed('file:///tmp/x')).toBe(true);
|
|
321
|
-
});
|
|
173
|
+
const HTTPS_ONLY: ShellExternalUrlPolicy = { allowedSchemes: ['https'] };
|
|
322
174
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
});
|
|
327
|
-
});
|
|
175
|
+
function beepHost(beep: ShellBeepBackend): HasShellBeep {
|
|
176
|
+
return { shell: { beep } };
|
|
177
|
+
}
|
|
328
178
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
setShellBackend(backend);
|
|
333
|
-
shellBeep();
|
|
334
|
-
expect(backend.beeped).toBe(1);
|
|
335
|
-
});
|
|
336
|
-
});
|
|
179
|
+
function externalHost(external: ShellExternalBackend): HasShellExternal {
|
|
180
|
+
return { shell: { external } };
|
|
181
|
+
}
|
|
337
182
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
setShellBackend(backend);
|
|
342
|
-
expect(await showItemInFolder('/tmp/x')).toBe(true);
|
|
343
|
-
expect(backend.opened).toBe('/tmp/x');
|
|
344
|
-
});
|
|
345
|
-
});
|
|
183
|
+
function pathOpenHost(pathOpen: ShellPathOpenBackend): HasShellPathOpen {
|
|
184
|
+
return { shell: { pathOpen } };
|
|
185
|
+
}
|
|
346
186
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
expect(result).toBe(false);
|
|
351
|
-
});
|
|
187
|
+
function pathRevealHost(pathReveal: ShellPathRevealBackend): HasShellPathReveal {
|
|
188
|
+
return { shell: { pathReveal } };
|
|
189
|
+
}
|
|
352
190
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
191
|
+
function shortcutLinkHost(shortcutLink: ShellShortcutLinkBackend): HasShellShortcutLink {
|
|
192
|
+
return { shell: { shortcutLink } };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function shortcutLinkProvider(): ShellShortcutLinkBackend & { write: ReturnType<typeof vi.fn> } {
|
|
196
|
+
return createEntity({
|
|
197
|
+
async read() {
|
|
198
|
+
return { link: { target: '/app' }, reason: 'ok' };
|
|
199
|
+
},
|
|
200
|
+
write: vi.fn(async () => ({ reason: 'ok' as const })),
|
|
201
|
+
} satisfies Omit<ShellShortcutLinkBackend, typeof EntityRuntimeKey>);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function trashHost(trash: ShellTrashBackend): HasShellTrash {
|
|
205
|
+
return { shell: { trash } };
|
|
206
|
+
}
|