@crewhaus/tool-desktop 0.7.0
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/dist/fixtures.d.ts +84 -0
- package/dist/fixtures.js +130 -0
- package/dist/host.d.ts +99 -0
- package/dist/host.js +174 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +1031 -0
- package/dist/lib/clipboard.d.ts +122 -0
- package/dist/lib/clipboard.js +230 -0
- package/dist/lib/escape.d.ts +220 -0
- package/dist/lib/escape.js +338 -0
- package/dist/lib/fsseam.d.ts +23 -0
- package/dist/lib/fsseam.js +73 -0
- package/dist/lib/notify.d.ts +110 -0
- package/dist/lib/notify.js +198 -0
- package/dist/lib/open.d.ts +149 -0
- package/dist/lib/open.js +421 -0
- package/dist/lib/outcome.d.ts +94 -0
- package/dist/lib/outcome.js +100 -0
- package/dist/lib/power.d.ts +150 -0
- package/dist/lib/power.js +224 -0
- package/dist/lib/presence.d.ts +118 -0
- package/dist/lib/presence.js +249 -0
- package/dist/lib/print.d.ts +120 -0
- package/dist/lib/print.js +188 -0
- package/dist/lib/windows.d.ts +100 -0
- package/dist/lib/windows.js +258 -0
- package/dist/paths.d.ts +42 -0
- package/dist/paths.js +158 -0
- package/dist/run.d.ts +187 -0
- package/dist/run.js +483 -0
- package/package.json +46 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorded output, and where each line came from.
|
|
3
|
+
*
|
|
4
|
+
* Nothing in this package's suite asks the machine it runs on. CI is a Linux
|
|
5
|
+
* container with no desktop at all, this was written on macOS, and a parser
|
|
6
|
+
* checked against whatever the test host happens to answer is a parser
|
|
7
|
+
* checked against nothing. So every classifier is fed from here.
|
|
8
|
+
*
|
|
9
|
+
* Each constant says whether it was RECORDED (somebody ran the command and
|
|
10
|
+
* pasted the bytes) or DOCUMENTED (written from the program's source or
|
|
11
|
+
* manual because nobody here has that platform). The distinction is kept
|
|
12
|
+
* deliberately: a documented fixture proves the parser handles the shape it
|
|
13
|
+
* was told about, and claims nothing more.
|
|
14
|
+
*/
|
|
15
|
+
/** No X server at all. Exit 1, empty stdout. */
|
|
16
|
+
export declare const XCLIP_NO_DISPLAY_STDERR = "Error: Can't open display: (null)\n";
|
|
17
|
+
/**
|
|
18
|
+
* A reachable display with nothing on the selection. Exit 1, empty stdout —
|
|
19
|
+
* the SAME exit code and the SAME empty stdout as the line above, and the
|
|
20
|
+
* opposite meaning. This pair is why `classifyClipboardRead` reads stderr.
|
|
21
|
+
*/
|
|
22
|
+
export declare const XCLIP_EMPTY_SELECTION_STDERR = "Error: target STRING not available\n";
|
|
23
|
+
/** The happy path. */
|
|
24
|
+
export declare const XCLIP_TEXT_STDOUT = "ssh-rsa AAAAB3NzaC1yc2EA... deploy@example\n";
|
|
25
|
+
/** `clipboard info` against an EMPTY pasteboard: empty stdout, exit 0. */
|
|
26
|
+
export declare const CLIPBOARD_INFO_EMPTY_STDOUT = "";
|
|
27
|
+
/**
|
|
28
|
+
* `clipboard info` with a screenshot on the pasteboard: flavour names and
|
|
29
|
+
* byte counts, and no content. This is what tells "empty" from "there is
|
|
30
|
+
* something here pbpaste cannot give you".
|
|
31
|
+
*/
|
|
32
|
+
export declare const CLIPBOARD_INFO_IMAGE_STDOUT = "\u00ABclass PNGf\u00BB, 284913, \u00ABclass 8BPS\u00BB, 1049928, TIFF picture, 1063968";
|
|
33
|
+
export declare const NOTIFY_SEND_NO_BUS_STDERR = "Cannot autolaunch D-Bus without X11 $DISPLAY\n";
|
|
34
|
+
/**
|
|
35
|
+
* The argument-injection this package's `--` exists to prevent: with the
|
|
36
|
+
* summary passed as a bare positional, `-u` was consumed as the urgency flag
|
|
37
|
+
* and the body became its argument.
|
|
38
|
+
*/
|
|
39
|
+
export declare const NOTIFY_SEND_FLAG_INJECTION_STDERR = "Unknown urgency body specified\n";
|
|
40
|
+
export declare const OSASCRIPT_NO_ACCESSIBILITY_STDERR = "44:87: execution error: System Events got an error: osascript is not allowed assistive access. (-25211)\n";
|
|
41
|
+
/** The OTHER grant, in a different System Settings pane. */
|
|
42
|
+
export declare const OSASCRIPT_NO_AUTOMATION_STDERR = "48:52: execution error: Not authorized to send Apple events to System Events. (-1743)\n";
|
|
43
|
+
/**
|
|
44
|
+
* `wmctrl -l -G -p -x`. Nine fixed columns then a title with spaces in it;
|
|
45
|
+
* the second row's title repeats a column's text on purpose, because a parser
|
|
46
|
+
* that re-joins the split fields gets that one wrong.
|
|
47
|
+
*/
|
|
48
|
+
export declare const WMCTRL_STDOUT: string;
|
|
49
|
+
/**
|
|
50
|
+
* `ioreg -r -c IOHIDSystem -d 1`. NANOSECONDS: 3_254_341_351_000 ns is 3254
|
|
51
|
+
* seconds. Reading it as milliseconds would report 3254 seconds of idleness
|
|
52
|
+
* as three, and make a locked machine look freshly used.
|
|
53
|
+
*
|
|
54
|
+
* The `-r` matters as much as the unit. `ioreg -c IOHIDSystem -d 1` — the
|
|
55
|
+
* form the survey sketch gave — prints this key NOWHERE on this OS version,
|
|
56
|
+
* because without `-r` the depth is measured from the root.
|
|
57
|
+
*/
|
|
58
|
+
export declare const IOREG_HIDIDLE_STDOUT: string;
|
|
59
|
+
/** The same machine, `ioreg -n Root -d1`, with the screen locked. */
|
|
60
|
+
export declare const IOREG_ROOT_LOCKED_STDOUT: string;
|
|
61
|
+
/** An unlocked console, same keys. */
|
|
62
|
+
export declare const IOREG_ROOT_UNLOCKED_STDOUT: string;
|
|
63
|
+
/** A macOS version that reports lock state somewhere this parser cannot see. */
|
|
64
|
+
export declare const IOREG_ROOT_NO_LOCK_KEY_STDOUT = " \"IORegistryPlanes\" = {\"IOService\"=\"IOService\"}";
|
|
65
|
+
/** DOCUMENTED: `loginctl show-session self -p LockedHint -p Type -p Active`. */
|
|
66
|
+
export declare const LOGINCTL_STDOUT = "LockedHint=no\nType=x11\nActive=yes\n";
|
|
67
|
+
/** DOCUMENTED: the Windows presence script's two lines. */
|
|
68
|
+
export declare const WINDOWS_PRESENCE_STDOUT = "idleMs=42000\nlocked=no\n";
|
|
69
|
+
/**
|
|
70
|
+
* `lpstat -p -d`, verbatim. Two printers in two DIFFERENT sentence shapes,
|
|
71
|
+
* an INDENTED continuation line that belongs to the row above it, and the
|
|
72
|
+
* default-destination line.
|
|
73
|
+
*/
|
|
74
|
+
export declare const LPSTAT_STDOUT: string;
|
|
75
|
+
/**
|
|
76
|
+
* A host whose scheduler is down. It lists NOTHING, which reads exactly like
|
|
77
|
+
* a host with no printers — the rule-6 case this fixture exists for.
|
|
78
|
+
*/
|
|
79
|
+
export declare const LPSTAT_NO_SCHEDULER_STDERR = "lpstat: Scheduler is not running.\n";
|
|
80
|
+
/** DOCUMENTED: `lp` answers with prose and the job id is inside it. */
|
|
81
|
+
export declare const LP_REQUEST_ID_STDOUT = "request id is Canon_MX490_series-34 (1 file(s))\n";
|
|
82
|
+
export declare const PS_CAFFEINATE_STDOUT = "caffeinate -i -m -t 1800\n";
|
|
83
|
+
/** The same pid, now owned by something else entirely. */
|
|
84
|
+
export declare const PS_REUSED_PID_STDOUT = "/usr/sbin/cupsd -l\n";
|
package/dist/fixtures.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Recorded output, and where each line came from.
|
|
3
|
+
*
|
|
4
|
+
* Nothing in this package's suite asks the machine it runs on. CI is a Linux
|
|
5
|
+
* container with no desktop at all, this was written on macOS, and a parser
|
|
6
|
+
* checked against whatever the test host happens to answer is a parser
|
|
7
|
+
* checked against nothing. So every classifier is fed from here.
|
|
8
|
+
*
|
|
9
|
+
* Each constant says whether it was RECORDED (somebody ran the command and
|
|
10
|
+
* pasted the bytes) or DOCUMENTED (written from the program's source or
|
|
11
|
+
* manual because nobody here has that platform). The distinction is kept
|
|
12
|
+
* deliberately: a documented fixture proves the parser handles the shape it
|
|
13
|
+
* was told about, and claims nothing more.
|
|
14
|
+
*/
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// clipboard — Linux, RECORDED on Debian 12 / xclip 0.13
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
/** No X server at all. Exit 1, empty stdout. */
|
|
19
|
+
export const XCLIP_NO_DISPLAY_STDERR = "Error: Can't open display: (null)\n";
|
|
20
|
+
/**
|
|
21
|
+
* A reachable display with nothing on the selection. Exit 1, empty stdout —
|
|
22
|
+
* the SAME exit code and the SAME empty stdout as the line above, and the
|
|
23
|
+
* opposite meaning. This pair is why `classifyClipboardRead` reads stderr.
|
|
24
|
+
*/
|
|
25
|
+
export const XCLIP_EMPTY_SELECTION_STDERR = "Error: target STRING not available\n";
|
|
26
|
+
/** The happy path. */
|
|
27
|
+
export const XCLIP_TEXT_STDOUT = "ssh-rsa AAAAB3NzaC1yc2EA... deploy@example\n";
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// clipboard — macOS, RECORDED on macOS 26.6.2
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
/** `clipboard info` against an EMPTY pasteboard: empty stdout, exit 0. */
|
|
32
|
+
export const CLIPBOARD_INFO_EMPTY_STDOUT = "";
|
|
33
|
+
/**
|
|
34
|
+
* `clipboard info` with a screenshot on the pasteboard: flavour names and
|
|
35
|
+
* byte counts, and no content. This is what tells "empty" from "there is
|
|
36
|
+
* something here pbpaste cannot give you".
|
|
37
|
+
*/
|
|
38
|
+
export const CLIPBOARD_INFO_IMAGE_STDOUT = "«class PNGf», 284913, «class 8BPS», 1049928, TIFF picture, 1063968";
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// notifications — Linux, RECORDED on Debian 12 / libnotify 0.8.1
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
export const NOTIFY_SEND_NO_BUS_STDERR = "Cannot autolaunch D-Bus without X11 $DISPLAY\n";
|
|
43
|
+
/**
|
|
44
|
+
* The argument-injection this package's `--` exists to prevent: with the
|
|
45
|
+
* summary passed as a bare positional, `-u` was consumed as the urgency flag
|
|
46
|
+
* and the body became its argument.
|
|
47
|
+
*/
|
|
48
|
+
export const NOTIFY_SEND_FLAG_INJECTION_STDERR = "Unknown urgency body specified\n";
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// AppleScript — RECORDED on macOS 26.6.2 with Accessibility NOT granted
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
export const OSASCRIPT_NO_ACCESSIBILITY_STDERR = "44:87: execution error: System Events got an error: osascript is not allowed assistive access. (-25211)\n";
|
|
53
|
+
/** The OTHER grant, in a different System Settings pane. */
|
|
54
|
+
export const OSASCRIPT_NO_AUTOMATION_STDERR = "48:52: execution error: Not authorized to send Apple events to System Events. (-1743)\n";
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// windows — DOCUMENTED (wmctrl 1.07 output format)
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* `wmctrl -l -G -p -x`. Nine fixed columns then a title with spaces in it;
|
|
60
|
+
* the second row's title repeats a column's text on purpose, because a parser
|
|
61
|
+
* that re-joins the split fields gets that one wrong.
|
|
62
|
+
*/
|
|
63
|
+
export const WMCTRL_STDOUT = [
|
|
64
|
+
"0x03c00003 0 2841 64 27 1280 800 Navigator.firefox buildbox Mozilla Firefox",
|
|
65
|
+
"0x04200005 0 3120 0 0 2560 1440 gnome-terminal-server.Gnome-terminal buildbox 0x04200005 build log",
|
|
66
|
+
"0x05000007 -1 4001 200 180 900 600 code.Code buildbox index.ts - factory",
|
|
67
|
+
].join("\n");
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// presence — RECORDED on macOS 26.6.2, screen locked, ~54 minutes idle
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
/**
|
|
72
|
+
* `ioreg -r -c IOHIDSystem -d 1`. NANOSECONDS: 3_254_341_351_000 ns is 3254
|
|
73
|
+
* seconds. Reading it as milliseconds would report 3254 seconds of idleness
|
|
74
|
+
* as three, and make a locked machine look freshly used.
|
|
75
|
+
*
|
|
76
|
+
* The `-r` matters as much as the unit. `ioreg -c IOHIDSystem -d 1` — the
|
|
77
|
+
* form the survey sketch gave — prints this key NOWHERE on this OS version,
|
|
78
|
+
* because without `-r` the depth is measured from the root.
|
|
79
|
+
*/
|
|
80
|
+
export const IOREG_HIDIDLE_STDOUT = [
|
|
81
|
+
"+-o IOHIDSystem <class IOHIDSystem, id 0x1000005b6, registered, matched, active>",
|
|
82
|
+
" | {",
|
|
83
|
+
' | "IOClass" = "IOHIDSystem"',
|
|
84
|
+
' | "HIDIdleTime" = 3254341351000',
|
|
85
|
+
' | "IOProviderClass" = "IOResources"',
|
|
86
|
+
" | }",
|
|
87
|
+
].join("\n");
|
|
88
|
+
/** The same machine, `ioreg -n Root -d1`, with the screen locked. */
|
|
89
|
+
export const IOREG_ROOT_LOCKED_STDOUT = [
|
|
90
|
+
' "IOConsoleLocked" = Yes',
|
|
91
|
+
' "IOConsoleUsers" = ({"kCGSSessionOnConsoleKey"=Yes,"kCGSSessionIDKey"=257,"CGSSessionScreenIsLocked"=Yes,"kCGSSessionUserIDKey"=506})',
|
|
92
|
+
].join("\n");
|
|
93
|
+
/** An unlocked console, same keys. */
|
|
94
|
+
export const IOREG_ROOT_UNLOCKED_STDOUT = [
|
|
95
|
+
' "IOConsoleLocked" = No',
|
|
96
|
+
' "IOConsoleUsers" = ({"kCGSSessionOnConsoleKey"=Yes,"CGSSessionScreenIsLocked"=No})',
|
|
97
|
+
].join("\n");
|
|
98
|
+
/** A macOS version that reports lock state somewhere this parser cannot see. */
|
|
99
|
+
export const IOREG_ROOT_NO_LOCK_KEY_STDOUT = ' "IORegistryPlanes" = {"IOService"="IOService"}';
|
|
100
|
+
/** DOCUMENTED: `loginctl show-session self -p LockedHint -p Type -p Active`. */
|
|
101
|
+
export const LOGINCTL_STDOUT = "LockedHint=no\nType=x11\nActive=yes\n";
|
|
102
|
+
/** DOCUMENTED: the Windows presence script's two lines. */
|
|
103
|
+
export const WINDOWS_PRESENCE_STDOUT = "idleMs=42000\nlocked=no\n";
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// printing — RECORDED on macOS 26.6.2, CUPS 2.4
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
/**
|
|
108
|
+
* `lpstat -p -d`, verbatim. Two printers in two DIFFERENT sentence shapes,
|
|
109
|
+
* an INDENTED continuation line that belongs to the row above it, and the
|
|
110
|
+
* default-destination line.
|
|
111
|
+
*/
|
|
112
|
+
export const LPSTAT_STDOUT = [
|
|
113
|
+
"printer Canon_MX490_series is idle. enabled since Sun Jul 26 10:59:45 2026",
|
|
114
|
+
"printer Canon_TS5300_series now printing Canon_TS5300_series-33. enabled since Sat Sep 12 00:00:20 2026",
|
|
115
|
+
"\tLooking for printer.",
|
|
116
|
+
"system default destination: Canon_MX490_series",
|
|
117
|
+
].join("\n");
|
|
118
|
+
/**
|
|
119
|
+
* A host whose scheduler is down. It lists NOTHING, which reads exactly like
|
|
120
|
+
* a host with no printers — the rule-6 case this fixture exists for.
|
|
121
|
+
*/
|
|
122
|
+
export const LPSTAT_NO_SCHEDULER_STDERR = "lpstat: Scheduler is not running.\n";
|
|
123
|
+
/** DOCUMENTED: `lp` answers with prose and the job id is inside it. */
|
|
124
|
+
export const LP_REQUEST_ID_STDOUT = "request id is Canon_MX490_series-34 (1 file(s))\n";
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
// power — DOCUMENTED (`ps -o command= -p <pid>`)
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
export const PS_CAFFEINATE_STDOUT = "caffeinate -i -m -t 1800\n";
|
|
129
|
+
/** The same pid, now owned by something else entirely. */
|
|
130
|
+
export const PS_REUSED_PID_STDOUT = "/usr/sbin/cupsd -l\n";
|
package/dist/host.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platforms this package makes decisions about.
|
|
3
|
+
*
|
|
4
|
+
* `"unsupported"` is a real member, not a placeholder: it is what every tool
|
|
5
|
+
* here returns `unavailable` for, and it is what an un-injected seam answers
|
|
6
|
+
* under test. Keeping it in the union means the exhaustive switch in each
|
|
7
|
+
* backend selector has to handle it, so a new tool cannot forget.
|
|
8
|
+
*/
|
|
9
|
+
export type HostPlatform = "darwin" | "linux" | "win32" | "unsupported";
|
|
10
|
+
/** What `process.platform` actually says. Only the smoke test asks. */
|
|
11
|
+
export declare function realHostPlatform(): HostPlatform;
|
|
12
|
+
export declare function hostPlatform(): HostPlatform;
|
|
13
|
+
/** Test seam: pretend to be another OS. */
|
|
14
|
+
export declare function _setPlatform(platform: HostPlatform | undefined): void;
|
|
15
|
+
/**
|
|
16
|
+
* The parts of the environment that decide whether a desktop is reachable.
|
|
17
|
+
*
|
|
18
|
+
* These are not configuration, they are *facts about the session*, and every
|
|
19
|
+
* one of them changes a tool's answer from "here it is" to "there is nothing
|
|
20
|
+
* to ask". Reading `process.env` directly inside a tool would mean a test's
|
|
21
|
+
* answer depended on whether the developer happened to be in an SSH session.
|
|
22
|
+
*/
|
|
23
|
+
export type SessionEnv = {
|
|
24
|
+
/** X11 display. Absent ⇒ `xclip`/`wmctrl` cannot connect at all. */
|
|
25
|
+
readonly DISPLAY: string | undefined;
|
|
26
|
+
/** Wayland socket name. */
|
|
27
|
+
readonly WAYLAND_DISPLAY: string | undefined;
|
|
28
|
+
/** `wl-paste` refuses without it even when WAYLAND_DISPLAY is set. */
|
|
29
|
+
readonly XDG_RUNTIME_DIR: string | undefined;
|
|
30
|
+
/** "x11" | "wayland" | "tty" on a logind session. */
|
|
31
|
+
readonly XDG_SESSION_TYPE: string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* The session bus `notify-send` posts to.
|
|
34
|
+
*
|
|
35
|
+
* Not in the first draft of this file, and its absence was a real gap: a
|
|
36
|
+
* notification does not go to the display server, it goes to a D-Bus
|
|
37
|
+
* service. With `DISPLAY` forwarded and this one dropped, libnotify falls
|
|
38
|
+
* back to autolaunching a bus — which on a headless box prints "Cannot
|
|
39
|
+
* autolaunch D-Bus without X11 $DISPLAY" and on a desktop box can start a
|
|
40
|
+
* SECOND private bus that no notification daemon is listening on, so the
|
|
41
|
+
* call succeeds and nothing is ever shown.
|
|
42
|
+
*/
|
|
43
|
+
readonly DBUS_SESSION_BUS_ADDRESS: string | undefined;
|
|
44
|
+
/** Set by sshd ⇒ this is a remote shell, not the console. */
|
|
45
|
+
readonly SSH_CONNECTION: string | undefined;
|
|
46
|
+
readonly SSH_TTY: string | undefined;
|
|
47
|
+
};
|
|
48
|
+
/** An environment with no desktop in it — the hostile default under test. */
|
|
49
|
+
export declare const HEADLESS_ENV: SessionEnv;
|
|
50
|
+
export declare function sessionEnv(): SessionEnv;
|
|
51
|
+
export declare function _setSessionEnv(env: SessionEnv | undefined): void;
|
|
52
|
+
/** True when this process is talking to a machine over ssh, not sitting at it. */
|
|
53
|
+
export declare function isRemoteShell(env?: SessionEnv): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Is there a graphical session a Linux backend could talk to, and which kind?
|
|
56
|
+
*
|
|
57
|
+
* Four answers, never two. "I could not tell" is not "there is no desktop":
|
|
58
|
+
* a Wayland session whose `XDG_RUNTIME_DIR` is unset really has no reachable
|
|
59
|
+
* compositor (recorded: `wl-paste` prints "XDG_RUNTIME_DIR is invalid or not
|
|
60
|
+
* set in the environment"), but a session with neither variable set may
|
|
61
|
+
* simply be a service manager that scrubbed the environment.
|
|
62
|
+
*/
|
|
63
|
+
export type LinuxSession = {
|
|
64
|
+
readonly kind: "x11";
|
|
65
|
+
readonly display: string;
|
|
66
|
+
} | {
|
|
67
|
+
readonly kind: "wayland";
|
|
68
|
+
readonly display: string;
|
|
69
|
+
readonly runtimeDir: string;
|
|
70
|
+
} | {
|
|
71
|
+
readonly kind: "wayland-unreachable";
|
|
72
|
+
readonly reason: string;
|
|
73
|
+
} | {
|
|
74
|
+
readonly kind: "none";
|
|
75
|
+
readonly reason: string;
|
|
76
|
+
};
|
|
77
|
+
export declare function linuxSession(env?: SessionEnv): LinuxSession;
|
|
78
|
+
/**
|
|
79
|
+
* The session variables a Linux desktop backend needs, forwarded explicitly.
|
|
80
|
+
*
|
|
81
|
+
* Explicitly, because `childEnv` copies nothing from `process.env` — and
|
|
82
|
+
* because an X11 client with no `DISPLAY` does not fail fast, it fails after
|
|
83
|
+
* a connection attempt or, on some builds, blocks until the deadline.
|
|
84
|
+
*/
|
|
85
|
+
export declare function linuxDisplayEnv(env: SessionEnv): Record<string, string>;
|
|
86
|
+
export type Clock = () => number;
|
|
87
|
+
/**
|
|
88
|
+
* Epoch milliseconds.
|
|
89
|
+
*
|
|
90
|
+
* `PowerAssertion` writes a deadline into a state file and reports when an
|
|
91
|
+
* assertion expires, so its whole contract is clock arithmetic. Reading
|
|
92
|
+
* `Date.now()` inside the tool would make every assertion about an expiry a
|
|
93
|
+
* race with the wall clock (house rule: never assert wall-clock timing —
|
|
94
|
+
* inject the clock instead).
|
|
95
|
+
*/
|
|
96
|
+
export declare function now(): number;
|
|
97
|
+
export declare function _setClock(clock: Clock | undefined): void;
|
|
98
|
+
/** Put every seam in this file back. Called from `afterEach`. */
|
|
99
|
+
export declare function _resetHostSeams(): void;
|
package/dist/host.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The seams, and the hostile default behind them.
|
|
3
|
+
*
|
|
4
|
+
* Every tool in this package reaches the operator's real desktop. That makes
|
|
5
|
+
* the machine the code runs on part of the answer twice over: the platform
|
|
6
|
+
* decides which backend exists at all (`pbpaste` is macOS-only, `wmctrl` is
|
|
7
|
+
* X11-only, `Out-Printer` is Windows-only), and the *session* decides whether
|
|
8
|
+
* that backend can do anything (an X11 clipboard with no `$DISPLAY` is not a
|
|
9
|
+
* slow clipboard, it is no clipboard).
|
|
10
|
+
*
|
|
11
|
+
* So the platform, the session environment, the clock and the child processes
|
|
12
|
+
* are all injected, and the suite never learns anything about the box it runs
|
|
13
|
+
* on except through a fixture somebody captured and pasted into `./fixtures`.
|
|
14
|
+
*
|
|
15
|
+
* ── THE HOSTILE DEFAULT ────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* `hostPlatform()` does not fall back to `process.platform`. It answers
|
|
18
|
+
* `"unsupported"` — a platform no backend in this package serves, on every
|
|
19
|
+
* machine, identically — whenever EITHER of two things is true:
|
|
20
|
+
*
|
|
21
|
+
* 1. A RUNNER IS INSTALLED. This is `@crewhaus/tool-pkgmgr`'s rule, and the
|
|
22
|
+
* primary guard. A test that has installed a recorded runner has already
|
|
23
|
+
* declared "I am not talking to this host"; if it then forgets to
|
|
24
|
+
* declare which host it is pretending to be, every backend refuses
|
|
25
|
+
* identically rather than taking the author's branch on macOS and a
|
|
26
|
+
* different one on a two-core Linux CI box.
|
|
27
|
+
* 2. THE REAL-HOST GATE IS SHUT (`run.ts`'s `_realHostAllowed`, false under
|
|
28
|
+
* `bun test`). The belt under the braces, for a test that never
|
|
29
|
+
* installed the seam at all. See `run.ts` for why this package is
|
|
30
|
+
* stricter than tool-pkgmgr about that case.
|
|
31
|
+
*
|
|
32
|
+
* That bug is not hypothetical: it cost this project a CI round in
|
|
33
|
+
* `tool-hostfs`, where one test relied on the ambient `process.platform`,
|
|
34
|
+
* passed on macOS and took a different branch on Linux. The cure is to make
|
|
35
|
+
* the ambient answer useless rather than to remember not to use it.
|
|
36
|
+
*
|
|
37
|
+
* `integration.test.ts` — the single test allowed near the real host — says so
|
|
38
|
+
* out loud by calling `_allowRealHost(true)`, and puts it back. It also
|
|
39
|
+
* asserts that the un-injected path then DOES read the real platform, so the
|
|
40
|
+
* guard above is provably a test-only condition rather than a package that
|
|
41
|
+
* refuses everywhere.
|
|
42
|
+
*/
|
|
43
|
+
import { _realHostAllowed, _runnerInstalled } from "./run";
|
|
44
|
+
let platformOverride;
|
|
45
|
+
function classifyPlatform(raw) {
|
|
46
|
+
if (raw === "darwin" || raw === "linux" || raw === "win32")
|
|
47
|
+
return raw;
|
|
48
|
+
return "unsupported";
|
|
49
|
+
}
|
|
50
|
+
/** What `process.platform` actually says. Only the smoke test asks. */
|
|
51
|
+
export function realHostPlatform() {
|
|
52
|
+
return classifyPlatform(process.platform);
|
|
53
|
+
}
|
|
54
|
+
export function hostPlatform() {
|
|
55
|
+
if (platformOverride !== undefined)
|
|
56
|
+
return platformOverride;
|
|
57
|
+
if (_runnerInstalled())
|
|
58
|
+
return "unsupported";
|
|
59
|
+
return _realHostAllowed() ? classifyPlatform(process.platform) : "unsupported";
|
|
60
|
+
}
|
|
61
|
+
/** Test seam: pretend to be another OS. */
|
|
62
|
+
export function _setPlatform(platform) {
|
|
63
|
+
platformOverride = platform;
|
|
64
|
+
}
|
|
65
|
+
const SESSION_KEYS = [
|
|
66
|
+
"DISPLAY",
|
|
67
|
+
"WAYLAND_DISPLAY",
|
|
68
|
+
"XDG_RUNTIME_DIR",
|
|
69
|
+
"XDG_SESSION_TYPE",
|
|
70
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
71
|
+
"SSH_CONNECTION",
|
|
72
|
+
"SSH_TTY",
|
|
73
|
+
];
|
|
74
|
+
/** An environment with no desktop in it — the hostile default under test. */
|
|
75
|
+
export const HEADLESS_ENV = Object.freeze({
|
|
76
|
+
DISPLAY: undefined,
|
|
77
|
+
WAYLAND_DISPLAY: undefined,
|
|
78
|
+
XDG_RUNTIME_DIR: undefined,
|
|
79
|
+
XDG_SESSION_TYPE: undefined,
|
|
80
|
+
DBUS_SESSION_BUS_ADDRESS: undefined,
|
|
81
|
+
SSH_CONNECTION: undefined,
|
|
82
|
+
SSH_TTY: undefined,
|
|
83
|
+
});
|
|
84
|
+
let envOverride;
|
|
85
|
+
export function sessionEnv() {
|
|
86
|
+
if (envOverride !== undefined)
|
|
87
|
+
return envOverride;
|
|
88
|
+
if (_runnerInstalled() || !_realHostAllowed())
|
|
89
|
+
return HEADLESS_ENV;
|
|
90
|
+
const read = (key) => {
|
|
91
|
+
const value = process.env[key];
|
|
92
|
+
// An EMPTY `DISPLAY` is not a display. Treating `""` as set is how a
|
|
93
|
+
// headless host gets classified as an X11 session and then blocks on a
|
|
94
|
+
// clipboard read until its deadline.
|
|
95
|
+
return value === undefined || value === "" ? undefined : value;
|
|
96
|
+
};
|
|
97
|
+
const out = {};
|
|
98
|
+
for (const key of SESSION_KEYS)
|
|
99
|
+
out[key] = read(key);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
export function _setSessionEnv(env) {
|
|
103
|
+
envOverride = env;
|
|
104
|
+
}
|
|
105
|
+
/** True when this process is talking to a machine over ssh, not sitting at it. */
|
|
106
|
+
export function isRemoteShell(env = sessionEnv()) {
|
|
107
|
+
return env.SSH_CONNECTION !== undefined || env.SSH_TTY !== undefined;
|
|
108
|
+
}
|
|
109
|
+
export function linuxSession(env = sessionEnv()) {
|
|
110
|
+
if (env.WAYLAND_DISPLAY !== undefined) {
|
|
111
|
+
if (env.XDG_RUNTIME_DIR === undefined) {
|
|
112
|
+
return {
|
|
113
|
+
kind: "wayland-unreachable",
|
|
114
|
+
reason: "WAYLAND_DISPLAY is set but XDG_RUNTIME_DIR is not, so the compositor socket cannot be located",
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
kind: "wayland",
|
|
119
|
+
display: env.WAYLAND_DISPLAY,
|
|
120
|
+
runtimeDir: env.XDG_RUNTIME_DIR,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (env.DISPLAY !== undefined)
|
|
124
|
+
return { kind: "x11", display: env.DISPLAY };
|
|
125
|
+
return {
|
|
126
|
+
kind: "none",
|
|
127
|
+
reason: "neither DISPLAY nor WAYLAND_DISPLAY is set in this process's environment, so there is no graphical session to reach",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The session variables a Linux desktop backend needs, forwarded explicitly.
|
|
132
|
+
*
|
|
133
|
+
* Explicitly, because `childEnv` copies nothing from `process.env` — and
|
|
134
|
+
* because an X11 client with no `DISPLAY` does not fail fast, it fails after
|
|
135
|
+
* a connection attempt or, on some builds, blocks until the deadline.
|
|
136
|
+
*/
|
|
137
|
+
export function linuxDisplayEnv(env) {
|
|
138
|
+
const out = {};
|
|
139
|
+
if (env.DISPLAY !== undefined)
|
|
140
|
+
out["DISPLAY"] = env.DISPLAY;
|
|
141
|
+
if (env.WAYLAND_DISPLAY !== undefined)
|
|
142
|
+
out["WAYLAND_DISPLAY"] = env.WAYLAND_DISPLAY;
|
|
143
|
+
if (env.XDG_RUNTIME_DIR !== undefined)
|
|
144
|
+
out["XDG_RUNTIME_DIR"] = env.XDG_RUNTIME_DIR;
|
|
145
|
+
if (env.DBUS_SESSION_BUS_ADDRESS !== undefined) {
|
|
146
|
+
out["DBUS_SESSION_BUS_ADDRESS"] = env.DBUS_SESSION_BUS_ADDRESS;
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
let clockOverride;
|
|
151
|
+
/**
|
|
152
|
+
* Epoch milliseconds.
|
|
153
|
+
*
|
|
154
|
+
* `PowerAssertion` writes a deadline into a state file and reports when an
|
|
155
|
+
* assertion expires, so its whole contract is clock arithmetic. Reading
|
|
156
|
+
* `Date.now()` inside the tool would make every assertion about an expiry a
|
|
157
|
+
* race with the wall clock (house rule: never assert wall-clock timing —
|
|
158
|
+
* inject the clock instead).
|
|
159
|
+
*/
|
|
160
|
+
export function now() {
|
|
161
|
+
return clockOverride === undefined ? Date.now() : clockOverride();
|
|
162
|
+
}
|
|
163
|
+
export function _setClock(clock) {
|
|
164
|
+
clockOverride = clock;
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// reset
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
/** Put every seam in this file back. Called from `afterEach`. */
|
|
170
|
+
export function _resetHostSeams() {
|
|
171
|
+
platformOverride = undefined;
|
|
172
|
+
envOverride = undefined;
|
|
173
|
+
clockOverride = undefined;
|
|
174
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RegisteredTool } from "@crewhaus/tool-catalog";
|
|
2
|
+
import { type HostPlatform } from "./host";
|
|
3
|
+
import { type UnknownFact } from "./lib/outcome";
|
|
4
|
+
export { _setRunner, _setDetacher, _resetRunSeams, _allowRealHost } from "./run";
|
|
5
|
+
export type { Runner, Detacher, RunRequest, RunResult, DetachResult } from "./run";
|
|
6
|
+
export { _setPlatform, _setSessionEnv, _setClock, _resetHostSeams, realHostPlatform, HEADLESS_ENV, } from "./host";
|
|
7
|
+
export type { HostPlatform, SessionEnv } from "./host";
|
|
8
|
+
export { _setFs } from "./lib/fsseam";
|
|
9
|
+
export type { HostFs, PathFacts } from "./lib/fsseam";
|
|
10
|
+
export { ToolPermissionError } from "./paths";
|
|
11
|
+
export declare const clipboardRead: RegisteredTool;
|
|
12
|
+
export declare const clipboardWrite: RegisteredTool;
|
|
13
|
+
export declare const desktopNotify: RegisteredTool;
|
|
14
|
+
export declare const openExternal: RegisteredTool;
|
|
15
|
+
export declare const printDocument: RegisteredTool;
|
|
16
|
+
export declare const windowList: RegisteredTool;
|
|
17
|
+
export declare const userPresence: RegisteredTool;
|
|
18
|
+
export declare const powerAssertion: RegisteredTool;
|
|
19
|
+
/** Everything this package registers, in one place for the catalog wiring. */
|
|
20
|
+
export declare const DESKTOP_TOOLS: ReadonlyArray<RegisteredTool>;
|
|
21
|
+
export type { UnknownFact, HostPlatform as DesktopPlatform };
|