@flighthq/shell 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.1047.858b9b6
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 +11 -19
- package/dist/shell.d.ts.map +1 -1
- package/dist/shell.js +30 -129
- package/dist/shell.js.map +1 -1
- package/package.json +4 -3
- package/src/shell.test.ts +240 -303
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @flighthq/shell
|
|
2
2
|
|
|
3
|
-
OS shell
|
|
3
|
+
OS shell commands over seven 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.1047.858b9b6](https://github.com/flighthq/flight/tree/858b9b601de814f6b6940cbb1ffff0916b903268/packages/shell)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/858b9b601de814f6b6940cbb1ffff0916b903268/LICENSE.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { isShellUrlAllowed, moveShellItemToTrash, moveShellItemsToTrash, openShellExternalUrl, openShellPath, readShellShortcutLink, revealShellPath, shellBeep, spawnShellProcess, 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,iBAAiB,EACjB,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, spawnShellProcess, 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,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
|
package/dist/shell.d.ts
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
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
|
|
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, ShellProcess, ShellProcessHost, ShellProcessOptions, 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 spawnShellProcess(host: ShellProcessHost, command: string, args: readonly string[], options?: Readonly<ShellProcessOptions>): ShellProcess | null;
|
|
11
|
+
export declare function writeShellShortcutLink(host: HasShellShortcutLink, shortcutPath: string, link: Readonly<ShellShortcutLink>, operation: ShellShortcutWriteOperation): Promise<ShellShortcutLinkWriteOutcome>;
|
|
20
12
|
//# 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,YAAY,EACZ,gBAAgB,EAChB,mBAAmB,EACnB,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,iBAAiB,CAC/B,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,CAAC,EAAE,QAAQ,CAAC,mBAAmB,CAAC,GACtC,YAAY,GAAG,IAAI,CAErB;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,46 @@
|
|
|
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 spawnShellProcess(host, command, args, options) {
|
|
41
|
+
return host.shell.process?.spawn(command, args, options) ?? null;
|
|
97
42
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
getShellBackend().beep();
|
|
43
|
+
export function writeShellShortcutLink(host, shortcutPath, link, operation) {
|
|
44
|
+
return host.shell.shortcutLink.write(shortcutPath, link, operation);
|
|
101
45
|
}
|
|
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
46
|
//# 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":"AAqBA,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,iBAAiB,CAC/B,IAAsB,EACtB,OAAe,EACf,IAAuB,EACvB,OAAuC;IAEvC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC;AACnE,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.1047.858b9b6",
|
|
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.1047.858b9b6"
|
|
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 seven explicit Host capability slots with required external URL policy",
|
|
47
48
|
"sideEffects": false
|
|
48
49
|
}
|
package/src/shell.test.ts
CHANGED
|
@@ -1,359 +1,296 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { allocateEntity, finishEntity } 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
|
+
ShellProcess,
|
|
16
|
+
ShellProcessBackend,
|
|
17
|
+
ShellProcessHost,
|
|
18
|
+
ShellProcessOptions,
|
|
19
|
+
ShellShortcutLinkBackend,
|
|
20
|
+
ShellTrashBackend,
|
|
21
|
+
} from '@flighthq/types/contract';
|
|
2
22
|
|
|
3
23
|
import {
|
|
4
|
-
explainShellBackend,
|
|
5
|
-
getShellBackend,
|
|
6
|
-
installShellHostBackend,
|
|
7
24
|
isShellUrlAllowed,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
observeShellHostResult,
|
|
25
|
+
moveShellItemsToTrash,
|
|
26
|
+
moveShellItemToTrash,
|
|
11
27
|
openShellExternalUrl,
|
|
12
28
|
openShellPath,
|
|
13
|
-
openShellPathResult,
|
|
14
29
|
readShellShortcutLink,
|
|
15
|
-
|
|
16
|
-
setShellBackend,
|
|
17
|
-
setShellUrlSchemeAllowlist,
|
|
30
|
+
revealShellPath,
|
|
18
31
|
shellBeep,
|
|
19
|
-
|
|
32
|
+
spawnShellProcess,
|
|
20
33
|
writeShellShortcutLink,
|
|
21
34
|
} from './shell';
|
|
22
35
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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');
|
|
36
|
+
describe('isShellUrlAllowed', () => {
|
|
37
|
+
it('matches schemes case-insensitively from an explicit policy', () => {
|
|
38
|
+
expect(isShellUrlAllowed('https://example.test', { allowedSchemes: ['HTTPS'] })).toBe(true);
|
|
39
|
+
expect(isShellUrlAllowed('mailto:user@example.test', { allowedSchemes: ['https'] })).toBe(false);
|
|
101
40
|
});
|
|
102
41
|
|
|
103
|
-
it('
|
|
104
|
-
|
|
105
|
-
expect(
|
|
42
|
+
it('blocks malformed URLs and an empty policy', () => {
|
|
43
|
+
expect(isShellUrlAllowed('not a URL', { allowedSchemes: ['https'] })).toBe(false);
|
|
44
|
+
expect(isShellUrlAllowed('https://example.test', { allowedSchemes: [] })).toBe(false);
|
|
106
45
|
});
|
|
107
46
|
});
|
|
108
47
|
|
|
109
|
-
describe('
|
|
110
|
-
it('
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
48
|
+
describe('moveShellItemsToTrash', () => {
|
|
49
|
+
it('starts every one-path operation and preserves input order while awaiting all results', async () => {
|
|
50
|
+
const paths: string[] = [];
|
|
51
|
+
const resolvers: Array<(outcome: { reason: 'ok' | 'operation-failed' }) => void> = [];
|
|
52
|
+
const host = trashHost(
|
|
53
|
+
(() => {
|
|
54
|
+
const out = allocateEntity<any>();
|
|
55
|
+
out.moveToTrash = (path: string) => {
|
|
56
|
+
paths.push(path);
|
|
57
|
+
return new Promise((resolve) => resolvers.push(resolve));
|
|
58
|
+
};
|
|
59
|
+
return finishEntity(out);
|
|
60
|
+
})(),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const outcomes = moveShellItemsToTrash(host, ['/first', '/second']);
|
|
64
|
+
expect(paths).toEqual(['/first', '/second']);
|
|
65
|
+
resolvers[1]?.({ reason: 'operation-failed' });
|
|
66
|
+
let settled = false;
|
|
67
|
+
void outcomes.then(() => {
|
|
68
|
+
settled = true;
|
|
69
|
+
});
|
|
70
|
+
await Promise.resolve();
|
|
71
|
+
expect(settled).toBe(false);
|
|
72
|
+
resolvers[0]?.({ reason: 'ok' });
|
|
73
|
+
await expect(outcomes).resolves.toEqual([{ reason: 'ok' }, { reason: 'operation-failed' }]);
|
|
118
74
|
});
|
|
119
75
|
});
|
|
120
76
|
|
|
121
|
-
describe('
|
|
122
|
-
it('
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
77
|
+
describe('moveShellItemToTrash', () => {
|
|
78
|
+
it('routes through the supplied trash provider', async () => {
|
|
79
|
+
const moveToTrash = vi.fn(async () => ({ reason: 'ok' as const }));
|
|
80
|
+
await expect(
|
|
81
|
+
moveShellItemToTrash(
|
|
82
|
+
trashHost(
|
|
83
|
+
(() => {
|
|
84
|
+
const out = allocateEntity<any>();
|
|
85
|
+
out.moveToTrash = moveToTrash;
|
|
86
|
+
return finishEntity(out);
|
|
87
|
+
})(),
|
|
88
|
+
),
|
|
89
|
+
'/item',
|
|
90
|
+
),
|
|
91
|
+
).resolves.toEqual({
|
|
92
|
+
reason: 'ok',
|
|
93
|
+
});
|
|
94
|
+
expect(moveToTrash).toHaveBeenCalledWith('/item');
|
|
135
95
|
});
|
|
136
96
|
});
|
|
137
97
|
|
|
138
|
-
describe('
|
|
139
|
-
it('
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
98
|
+
describe('openShellExternalUrl', () => {
|
|
99
|
+
it('requires policy as the third parameter at the type boundary', () => {
|
|
100
|
+
expectTypeOf(openShellExternalUrl).parameters.toEqualTypeOf<
|
|
101
|
+
[HasShellExternal, string, Readonly<ShellExternalUrlPolicy>]
|
|
102
|
+
>();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('blocks before dispatch when the scheme is not allowed', async () => {
|
|
106
|
+
const open = vi.fn(async () => ({ reason: 'ok' as const }));
|
|
107
|
+
const host = externalHost(
|
|
108
|
+
(() => {
|
|
109
|
+
const out = allocateEntity<any>();
|
|
110
|
+
out.open = open;
|
|
111
|
+
return finishEntity(out);
|
|
112
|
+
})(),
|
|
113
|
+
);
|
|
114
|
+
await expect(openShellExternalUrl(host, 'file:///etc/passwd', { allowedSchemes: ['https'] })).resolves.toEqual({
|
|
115
|
+
reason: 'blocked-scheme',
|
|
116
|
+
});
|
|
117
|
+
expect(open).not.toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('keeps two live hosts isolated', async () => {
|
|
121
|
+
const calls: string[] = [];
|
|
122
|
+
const first = externalHost(
|
|
123
|
+
(() => {
|
|
124
|
+
const out = allocateEntity<any>();
|
|
125
|
+
out.open = async (url: string) => {
|
|
126
|
+
calls.push(`first:${url}`);
|
|
127
|
+
return { reason: 'ok' };
|
|
128
|
+
};
|
|
129
|
+
return finishEntity(out);
|
|
130
|
+
})(),
|
|
131
|
+
);
|
|
132
|
+
const second = externalHost(
|
|
133
|
+
(() => {
|
|
134
|
+
const out = allocateEntity<any>();
|
|
135
|
+
out.open = async (url: string) => {
|
|
136
|
+
calls.push(`second:${url}`);
|
|
137
|
+
return { reason: 'operation-failed' };
|
|
138
|
+
};
|
|
139
|
+
return finishEntity(out);
|
|
140
|
+
})(),
|
|
141
|
+
);
|
|
142
|
+
await expect(openShellExternalUrl(first, 'https://one.test', HTTPS_ONLY)).resolves.toEqual({ reason: 'ok' });
|
|
143
|
+
await expect(openShellExternalUrl(second, 'https://two.test', HTTPS_ONLY)).resolves.toEqual({
|
|
144
|
+
reason: 'operation-failed',
|
|
145
|
+
});
|
|
146
|
+
expect(calls).toEqual(['first:https://one.test', 'second:https://two.test']);
|
|
159
147
|
});
|
|
160
148
|
});
|
|
161
149
|
|
|
162
|
-
describe('
|
|
163
|
-
it('
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
expect(
|
|
150
|
+
describe('openShellPath', () => {
|
|
151
|
+
it('preserves a provider failure and its message', async () => {
|
|
152
|
+
const provider = allocateEntity<ShellPathOpenBackend>();
|
|
153
|
+
provider.open = async () => {
|
|
154
|
+
return { message: '', reason: 'operation-failed' as const };
|
|
155
|
+
};
|
|
156
|
+
await expect(openShellPath(pathOpenHost(finishEntity(provider)), '/missing')).resolves.toEqual({
|
|
157
|
+
message: '',
|
|
158
|
+
reason: 'operation-failed',
|
|
159
|
+
});
|
|
169
160
|
});
|
|
170
161
|
});
|
|
171
162
|
|
|
172
|
-
describe('
|
|
173
|
-
it('
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
163
|
+
describe('readShellShortcutLink', () => {
|
|
164
|
+
it('returns the read-specific outcome', async () => {
|
|
165
|
+
const provider = shortcutLinkProvider();
|
|
166
|
+
await expect(readShellShortcutLink(shortcutLinkHost(provider), '/app.lnk')).resolves.toEqual({
|
|
167
|
+
link: { target: '/app' },
|
|
168
|
+
reason: 'ok',
|
|
169
|
+
});
|
|
178
170
|
});
|
|
179
171
|
});
|
|
180
172
|
|
|
181
|
-
describe('
|
|
182
|
-
it('
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
173
|
+
describe('revealShellPath', () => {
|
|
174
|
+
it('returns the reveal-specific outcome', async () => {
|
|
175
|
+
const reveal = vi.fn(async () => ({ reason: 'operation-failed' as const }));
|
|
176
|
+
await expect(
|
|
177
|
+
revealShellPath(
|
|
178
|
+
pathRevealHost(
|
|
179
|
+
(() => {
|
|
180
|
+
const out = allocateEntity<any>();
|
|
181
|
+
out.reveal = reveal;
|
|
182
|
+
return finishEntity(out);
|
|
183
|
+
})(),
|
|
184
|
+
),
|
|
185
|
+
'/item',
|
|
186
|
+
),
|
|
187
|
+
).resolves.toEqual({
|
|
188
|
+
reason: 'operation-failed',
|
|
189
|
+
});
|
|
196
190
|
});
|
|
197
191
|
});
|
|
198
192
|
|
|
199
|
-
describe('
|
|
200
|
-
it('
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
expect(
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('returns false and does not call the backend when the URL scheme is blocked', async () => {
|
|
215
|
-
const backend = fakeBackend();
|
|
216
|
-
setShellBackend(backend);
|
|
217
|
-
setShellUrlSchemeAllowlist(['https']);
|
|
218
|
-
expect(await openShellExternalUrl('file:///etc/passwd')).toBe(false);
|
|
219
|
-
expect(backend.opened).toBe('');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('allows the URL when its scheme is in the allowlist', async () => {
|
|
223
|
-
const backend = fakeBackend();
|
|
224
|
-
setShellBackend(backend);
|
|
225
|
-
setShellUrlSchemeAllowlist(['https', 'mailto']);
|
|
226
|
-
expect(await openShellExternalUrl('https://example.com')).toBe(true);
|
|
227
|
-
expect(backend.opened).toBe('https://example.com');
|
|
193
|
+
describe('shellBeep', () => {
|
|
194
|
+
it('dispatches synchronously', () => {
|
|
195
|
+
const beep = vi.fn();
|
|
196
|
+
shellBeep(
|
|
197
|
+
beepHost(
|
|
198
|
+
(() => {
|
|
199
|
+
const out = allocateEntity<any>();
|
|
200
|
+
out.beep = beep;
|
|
201
|
+
return finishEntity(out);
|
|
202
|
+
})(),
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
expect(beep).toHaveBeenCalledOnce();
|
|
228
206
|
});
|
|
229
207
|
});
|
|
230
208
|
|
|
231
|
-
describe('
|
|
232
|
-
it('
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
209
|
+
describe('spawnShellProcess', () => {
|
|
210
|
+
it('exposes the explicit host, command, argument-vector, and optional-options API', () => {
|
|
211
|
+
expectTypeOf(spawnShellProcess).parameters.toEqualTypeOf<
|
|
212
|
+
[ShellProcessHost, string, readonly string[], Readonly<ShellProcessOptions>?]
|
|
213
|
+
>();
|
|
214
|
+
expectTypeOf(spawnShellProcess).returns.toEqualTypeOf<ShellProcess | null>();
|
|
237
215
|
});
|
|
238
216
|
|
|
239
|
-
it('forwards the
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
217
|
+
it('forwards the command, argument vector, and options to the host backend unchanged', () => {
|
|
218
|
+
const process = shellProcess();
|
|
219
|
+
const spawn = vi.fn(() => process);
|
|
220
|
+
const host = processHost(
|
|
221
|
+
(() => {
|
|
222
|
+
const out = allocateEntity<any>();
|
|
223
|
+
out.spawn = spawn;
|
|
224
|
+
return finishEntity(out);
|
|
225
|
+
})(),
|
|
226
|
+
);
|
|
227
|
+
const args = ['--flag', 'value'] as const;
|
|
228
|
+
const options = { cwd: '/work', environment: { MODE: 'test' } } as const;
|
|
245
229
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
setShellBackend(backend);
|
|
249
|
-
await openShellPath('/tmp/x', { application: 'TextEdit' });
|
|
250
|
-
expect(backend.lastOptions).toEqual({ application: 'TextEdit' });
|
|
230
|
+
expect(spawnShellProcess(host, '/bin/tool', args, options)).toBe(process);
|
|
231
|
+
expect(spawn).toHaveBeenCalledWith('/bin/tool', args, options);
|
|
251
232
|
});
|
|
252
233
|
|
|
253
|
-
it('
|
|
254
|
-
|
|
255
|
-
setShellBackend(backend);
|
|
256
|
-
await openShellPath('/tmp/x');
|
|
257
|
-
expect(backend.lastOptions).toBeUndefined();
|
|
234
|
+
it('returns null when the host does not expose child-process support', () => {
|
|
235
|
+
expect(spawnShellProcess({ shell: {} }, '/bin/tool', [])).toBeNull();
|
|
258
236
|
});
|
|
259
237
|
});
|
|
260
238
|
|
|
261
|
-
describe('
|
|
262
|
-
it('
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
expect(
|
|
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' });
|
|
239
|
+
describe('writeShellShortcutLink', () => {
|
|
240
|
+
it('requires an explicit write operation and returns the write-specific outcome', async () => {
|
|
241
|
+
const provider = shortcutLinkProvider();
|
|
242
|
+
await expect(
|
|
243
|
+
writeShellShortcutLink(shortcutLinkHost(provider), '/app.lnk', { target: '/app' }, 'replace'),
|
|
244
|
+
).resolves.toEqual({ reason: 'ok' });
|
|
245
|
+
expect(provider.write).toHaveBeenCalledWith('/app.lnk', { target: '/app' }, 'replace');
|
|
282
246
|
});
|
|
283
247
|
});
|
|
284
248
|
|
|
285
|
-
|
|
286
|
-
it('returns null with sentinel (no backend set)', async () => {
|
|
287
|
-
const result = await readShellShortcutLink('/tmp/x.lnk');
|
|
288
|
-
expect(result).toBeNull();
|
|
289
|
-
});
|
|
249
|
+
const HTTPS_ONLY: ShellExternalUrlPolicy = { allowedSchemes: ['https'] };
|
|
290
250
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const result = await readShellShortcutLink('/tmp/x.lnk');
|
|
295
|
-
expect(result).toEqual({ target: '/path/to/target' });
|
|
296
|
-
});
|
|
297
|
-
});
|
|
251
|
+
function beepHost(beep: ShellBeepBackend): HasShellBeep {
|
|
252
|
+
return { shell: { beep } };
|
|
253
|
+
}
|
|
298
254
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
installShellHostBackend(fakeBackend());
|
|
303
|
-
resetShellBackendForTest();
|
|
304
|
-
expect(explainShellBackend().layer).toBe('host-not-enabled');
|
|
305
|
-
});
|
|
306
|
-
});
|
|
255
|
+
function externalHost(external: ShellExternalBackend): HasShellExternal {
|
|
256
|
+
return { shell: { external } };
|
|
257
|
+
}
|
|
307
258
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
setShellBackend(null);
|
|
312
|
-
expect(getShellBackend()).not.toBeNull();
|
|
313
|
-
});
|
|
314
|
-
});
|
|
259
|
+
function pathOpenHost(pathOpen: ShellPathOpenBackend): HasShellPathOpen {
|
|
260
|
+
return { shell: { pathOpen } };
|
|
261
|
+
}
|
|
315
262
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
setShellUrlSchemeAllowlist(null);
|
|
320
|
-
expect(isShellUrlAllowed('file:///tmp/x')).toBe(true);
|
|
321
|
-
});
|
|
263
|
+
function pathRevealHost(pathReveal: ShellPathRevealBackend): HasShellPathReveal {
|
|
264
|
+
return { shell: { pathReveal } };
|
|
265
|
+
}
|
|
322
266
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
});
|
|
327
|
-
});
|
|
267
|
+
function processHost(process: ShellProcessBackend): ShellProcessHost {
|
|
268
|
+
return { shell: { process } };
|
|
269
|
+
}
|
|
328
270
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
271
|
+
function shellProcess(): ShellProcess {
|
|
272
|
+
const out = allocateEntity<any>();
|
|
273
|
+
out.exit = Promise.resolve({ code: 0, signal: null });
|
|
274
|
+
out.stderr = new ReadableStream<Uint8Array>();
|
|
275
|
+
out.stdin = new WritableStream<Uint8Array>();
|
|
276
|
+
out.stdout = new ReadableStream<Uint8Array>();
|
|
277
|
+
out.terminate = vi.fn();
|
|
278
|
+
return finishEntity(out);
|
|
279
|
+
}
|
|
337
280
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
setShellBackend(backend);
|
|
342
|
-
expect(await showItemInFolder('/tmp/x')).toBe(true);
|
|
343
|
-
expect(backend.opened).toBe('/tmp/x');
|
|
344
|
-
});
|
|
345
|
-
});
|
|
281
|
+
function shortcutLinkHost(shortcutLink: ShellShortcutLinkBackend): HasShellShortcutLink {
|
|
282
|
+
return { shell: { shortcutLink } };
|
|
283
|
+
}
|
|
346
284
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
285
|
+
function shortcutLinkProvider(): ShellShortcutLinkBackend & { write: ReturnType<typeof vi.fn> } {
|
|
286
|
+
const out = allocateEntity<any>();
|
|
287
|
+
out.read = async () => {
|
|
288
|
+
return { link: { target: '/app' }, reason: 'ok' };
|
|
289
|
+
};
|
|
290
|
+
out.write = vi.fn(async () => ({ reason: 'ok' as const }));
|
|
291
|
+
return finishEntity(out);
|
|
292
|
+
}
|
|
352
293
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const result = await writeShellShortcutLink('/tmp/x.lnk', { target: '/tmp/target' }, 'create');
|
|
357
|
-
expect(result).toBe(true);
|
|
358
|
-
});
|
|
359
|
-
});
|
|
294
|
+
function trashHost(trash: ShellTrashBackend): HasShellTrash {
|
|
295
|
+
return { shell: { trash } };
|
|
296
|
+
}
|