@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.
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Clipboard backends, and the one distinction the rest of the package is
3
+ * shaped around: an EMPTY clipboard is not an UNREADABLE one.
4
+ *
5
+ * Recorded on Debian 12 / xclip 0.13, in a container with no X server and
6
+ * then under Xvfb:
7
+ *
8
+ * no display `xclip -selection clipboard -o` -> exit 1,
9
+ * "Error: Can't open display: (null)"
10
+ * display, nothing `xclip -selection primary -o` -> exit 1,
11
+ * on the selection "Error: target STRING
12
+ * not available"
13
+ * display, text `xclip -selection clipboard -o` -> exit 0, the text
14
+ *
15
+ * The first two are the same exit code and the same empty stdout. A backend
16
+ * that reports "the clipboard is empty" for both tells a workflow that the
17
+ * operator has copied nothing when the truth is that nothing was asked.
18
+ *
19
+ * macOS is the mirror image: `pbpaste` exits 0 with empty stdout whether the
20
+ * clipboard is empty or holds an image with no text flavour. So the macOS
21
+ * reader disambiguates with a SECOND probe, `clipboard info`, and only when
22
+ * the first one came back empty - never on the happy path.
23
+ */
24
+ import type { SessionEnv } from "../host";
25
+ import { type HostPlatform } from "../host";
26
+ import type { RunRequest } from "../run";
27
+ import { type Unavailable } from "./outcome";
28
+ export type ClipboardFormat = "text" | "html";
29
+ /**
30
+ * `clipboard info` names the FLAVOURS on the pasteboard and their byte
31
+ * counts. It never returns content, which is why it is safe to run as a
32
+ * disambiguating probe: the tool learns "there is something there" without
33
+ * putting whatever it is into a context window.
34
+ *
35
+ * Recorded on macOS 26.6.2 against an EMPTY clipboard: empty stdout, exit 0.
36
+ */
37
+ export declare const CLIPBOARD_INFO_SCRIPT: import("./escape").AppleScript;
38
+ /** Windows readers. Two registered scripts, chosen from a closed enum - the
39
+ * format is never interpolated into one script. */
40
+ export declare const WINDOWS_CLIPBOARD_READ_TEXT: import("./escape").PowerShellScript;
41
+ export declare const WINDOWS_CLIPBOARD_READ_HTML: import("./escape").PowerShellScript;
42
+ /**
43
+ * The Windows disambiguating probe, and the reason it exists.
44
+ *
45
+ * `Get-Clipboard -Raw` returns `$null` for an image or a file-drop exactly as
46
+ * it does for an empty clipboard, so Windows has the SAME ambiguity macOS has
47
+ * and an earlier draft of this file only handled the macOS half. This names
48
+ * the non-text flavours without returning their content, mirroring
49
+ * `clipboard info`.
50
+ *
51
+ * DOCUMENTED, NOT RECORDED: `-Format Image|FileDropList|Audio` is a
52
+ * PowerShell 5.1 parameter set, but nobody here has a Windows box to capture
53
+ * it on. It is written to fail safe — any error or unexpected output leaves
54
+ * `classifyClipboardInfo` reading empty stdout, which is the answer the
55
+ * primary read already gave.
56
+ */
57
+ export declare const WINDOWS_CLIPBOARD_FORMATS: import("./escape").PowerShellScript;
58
+ /**
59
+ * The Windows writer takes the payload on STDIN, not as a parameter.
60
+ *
61
+ * Argv is world-readable through a process listing on every platform this
62
+ * package supports, and a clipboard payload is exactly the sort of thing an
63
+ * operator would not want there. It is also, on Windows specifically, the
64
+ * channel that would put the value back into PowerShell source.
65
+ */
66
+ export declare const WINDOWS_CLIPBOARD_WRITE: import("./escape").PowerShellScript;
67
+ export type ClipboardPlan = {
68
+ readonly ok: true;
69
+ /** Which backend program was chosen, for the result and for `dryRun`. */
70
+ readonly backend: string;
71
+ readonly request: Omit<RunRequest, "timeoutMs">;
72
+ /**
73
+ * Run only when the primary read came back empty, to tell an empty
74
+ * clipboard from one holding a flavour the reader cannot return.
75
+ * macOS and Windows both need it; the Linux backends report the
76
+ * difference in their own stderr, so neither carries one.
77
+ */
78
+ readonly disambiguate?: Omit<RunRequest, "timeoutMs">;
79
+ } | {
80
+ readonly ok: false;
81
+ readonly unavailable: Unavailable;
82
+ };
83
+ export declare function planClipboardRead(platform: HostPlatform, env: SessionEnv, format: ClipboardFormat): ClipboardPlan;
84
+ export declare function planClipboardWrite(platform: HostPlatform, env: SessionEnv, payload: string): ClipboardPlan;
85
+ /**
86
+ * What a clipboard READ actually found.
87
+ *
88
+ * Four outcomes, not two. `noTextFlavour` is the one people forget: a
89
+ * screenshot on the clipboard makes `pbpaste` print nothing at all, and
90
+ * reporting that as "empty" sends a workflow off to ask the operator to copy
91
+ * something they have already copied.
92
+ */
93
+ export type ClipboardReadOutcome = {
94
+ readonly outcome: "read";
95
+ readonly text: string;
96
+ } | {
97
+ readonly outcome: "empty";
98
+ readonly reason: string;
99
+ } | {
100
+ readonly outcome: "noTextFlavour";
101
+ readonly reason: string;
102
+ } | Unavailable | {
103
+ readonly outcome: "failed";
104
+ readonly reason: string;
105
+ };
106
+ export declare function classifyClipboardRead(result: {
107
+ readonly code: number;
108
+ readonly stdout: string;
109
+ readonly stderr: string;
110
+ readonly timedOut: boolean;
111
+ readonly missing: boolean;
112
+ readonly refused?: boolean;
113
+ readonly stdoutTruncated?: boolean;
114
+ }): ClipboardReadOutcome;
115
+ /**
116
+ * Read the macOS disambiguating probe.
117
+ *
118
+ * Empty output means the pasteboard genuinely holds nothing (recorded).
119
+ * Non-empty means it holds SOMETHING that `pbpaste` would not give us, which
120
+ * is a different answer and a different instruction to the caller.
121
+ */
122
+ export declare function classifyClipboardInfo(stdout: string): "empty" | "noTextFlavour";
@@ -0,0 +1,230 @@
1
+ import { linuxDisplayEnv, linuxSession } from "../host";
2
+ import { osascriptArgv, powershellArgv, registerAppleScript, registerPowerShellScript, } from "./escape";
3
+ import { unavailable, unsupportedPlatform } from "./outcome";
4
+ /**
5
+ * `clipboard info` names the FLAVOURS on the pasteboard and their byte
6
+ * counts. It never returns content, which is why it is safe to run as a
7
+ * disambiguating probe: the tool learns "there is something there" without
8
+ * putting whatever it is into a context window.
9
+ *
10
+ * Recorded on macOS 26.6.2 against an EMPTY clipboard: empty stdout, exit 0.
11
+ */
12
+ export const CLIPBOARD_INFO_SCRIPT = registerAppleScript({
13
+ name: "clipboard-info",
14
+ lines: ["return (clipboard info) as string"],
15
+ arity: 0,
16
+ });
17
+ /** Windows readers. Two registered scripts, chosen from a closed enum - the
18
+ * format is never interpolated into one script. */
19
+ export const WINDOWS_CLIPBOARD_READ_TEXT = registerPowerShellScript({
20
+ name: "clipboard-read-text",
21
+ script: "$t = Get-Clipboard -Raw; if ($null -ne $t) { [Console]::Out.Write($t) }",
22
+ reads: [],
23
+ });
24
+ export const WINDOWS_CLIPBOARD_READ_HTML = registerPowerShellScript({
25
+ name: "clipboard-read-html",
26
+ script: "$t = Get-Clipboard -TextFormatType Html -Raw; if ($null -ne $t) { [Console]::Out.Write($t) }",
27
+ reads: [],
28
+ });
29
+ /**
30
+ * The Windows disambiguating probe, and the reason it exists.
31
+ *
32
+ * `Get-Clipboard -Raw` returns `$null` for an image or a file-drop exactly as
33
+ * it does for an empty clipboard, so Windows has the SAME ambiguity macOS has
34
+ * and an earlier draft of this file only handled the macOS half. This names
35
+ * the non-text flavours without returning their content, mirroring
36
+ * `clipboard info`.
37
+ *
38
+ * DOCUMENTED, NOT RECORDED: `-Format Image|FileDropList|Audio` is a
39
+ * PowerShell 5.1 parameter set, but nobody here has a Windows box to capture
40
+ * it on. It is written to fail safe — any error or unexpected output leaves
41
+ * `classifyClipboardInfo` reading empty stdout, which is the answer the
42
+ * primary read already gave.
43
+ */
44
+ export const WINDOWS_CLIPBOARD_FORMATS = registerPowerShellScript({
45
+ name: "clipboard-formats",
46
+ script: "$f = @(); foreach ($k in 'Image','FileDropList','Audio') { try { if (Get-Clipboard -Format $k) { $f += $k } } catch {} }; [Console]::Out.Write($f -join ',')",
47
+ reads: [],
48
+ });
49
+ /**
50
+ * The Windows writer takes the payload on STDIN, not as a parameter.
51
+ *
52
+ * Argv is world-readable through a process listing on every platform this
53
+ * package supports, and a clipboard payload is exactly the sort of thing an
54
+ * operator would not want there. It is also, on Windows specifically, the
55
+ * channel that would put the value back into PowerShell source.
56
+ */
57
+ export const WINDOWS_CLIPBOARD_WRITE = registerPowerShellScript({
58
+ name: "clipboard-write",
59
+ script: "Set-Clipboard -Value ([Console]::In.ReadToEnd())",
60
+ reads: [],
61
+ });
62
+ export function planClipboardRead(platform, env, format) {
63
+ switch (platform) {
64
+ case "darwin": {
65
+ if (format === "html") {
66
+ return {
67
+ ok: false,
68
+ unavailable: unavailable("program", 'pbpaste exposes only the plain-text flavour; the HTML flavour needs an osascript «class HTML» round-trip that returns hex, which this package does not decode - ask for format "text"', "pbpaste"),
69
+ };
70
+ }
71
+ return {
72
+ ok: true,
73
+ backend: "pbpaste",
74
+ request: { argv: ["pbpaste"] },
75
+ disambiguate: { argv: osascriptArgv(CLIPBOARD_INFO_SCRIPT) },
76
+ };
77
+ }
78
+ case "linux": {
79
+ const session = linuxSession(env);
80
+ if (session.kind === "none" || session.kind === "wayland-unreachable") {
81
+ return { ok: false, unavailable: unavailable("session", session.reason) };
82
+ }
83
+ const displayEnv = linuxDisplayEnv(env);
84
+ if (session.kind === "wayland") {
85
+ return {
86
+ ok: true,
87
+ backend: "wl-paste",
88
+ request: {
89
+ argv: format === "html"
90
+ ? ["wl-paste", "--no-newline", "--type", "text/html"]
91
+ : ["wl-paste", "--no-newline"],
92
+ env: displayEnv,
93
+ },
94
+ };
95
+ }
96
+ return {
97
+ ok: true,
98
+ backend: "xclip",
99
+ request: {
100
+ argv: format === "html"
101
+ ? ["xclip", "-selection", "clipboard", "-o", "-t", "text/html"]
102
+ : ["xclip", "-selection", "clipboard", "-o"],
103
+ env: displayEnv,
104
+ },
105
+ };
106
+ }
107
+ case "win32": {
108
+ const script = format === "html" ? WINDOWS_CLIPBOARD_READ_HTML : WINDOWS_CLIPBOARD_READ_TEXT;
109
+ const built = powershellArgv(script, {});
110
+ return {
111
+ ok: true,
112
+ backend: "powershell Get-Clipboard",
113
+ request: { argv: built.argv },
114
+ disambiguate: { argv: powershellArgv(WINDOWS_CLIPBOARD_FORMATS, {}).argv },
115
+ };
116
+ }
117
+ default:
118
+ return { ok: false, unavailable: unsupportedPlatform(platform, "clipboard") };
119
+ }
120
+ }
121
+ export function planClipboardWrite(platform, env, payload) {
122
+ switch (platform) {
123
+ case "darwin":
124
+ return { ok: true, backend: "pbcopy", request: { argv: ["pbcopy"], stdin: payload } };
125
+ case "linux": {
126
+ const session = linuxSession(env);
127
+ if (session.kind === "none" || session.kind === "wayland-unreachable") {
128
+ return { ok: false, unavailable: unavailable("session", session.reason) };
129
+ }
130
+ const displayEnv = linuxDisplayEnv(env);
131
+ if (session.kind === "wayland") {
132
+ return {
133
+ ok: true,
134
+ backend: "wl-copy",
135
+ request: { argv: ["wl-copy"], stdin: payload, env: displayEnv },
136
+ };
137
+ }
138
+ // Under X11 there is no server-side clipboard store: the writing
139
+ // process OWNS the selection and must outlive this call or the content
140
+ // vanishes the moment the child exits. `xclip -i` forks a resident
141
+ // holder by itself, which is the behaviour that makes this work - and
142
+ // the reason the runner's stream drain is BOUNDED, because that holder
143
+ // inherits the stdout pipe and never closes it.
144
+ return {
145
+ ok: true,
146
+ backend: "xclip",
147
+ request: {
148
+ argv: ["xclip", "-selection", "clipboard", "-i"],
149
+ stdin: payload,
150
+ env: displayEnv,
151
+ },
152
+ };
153
+ }
154
+ case "win32": {
155
+ const built = powershellArgv(WINDOWS_CLIPBOARD_WRITE, {});
156
+ return {
157
+ ok: true,
158
+ backend: "powershell Set-Clipboard",
159
+ request: { argv: built.argv, stdin: payload },
160
+ };
161
+ }
162
+ default:
163
+ return { ok: false, unavailable: unsupportedPlatform(platform, "clipboard") };
164
+ }
165
+ }
166
+ /** Stderr patterns that mean "there is no display to talk to", per backend. */
167
+ const NO_SESSION = [
168
+ /Can't open display/i,
169
+ /Failed to connect to a Wayland server/i,
170
+ /XDG_RUNTIME_DIR is invalid or not set/i,
171
+ /Unable to init server/i,
172
+ ];
173
+ /** Stderr patterns that mean "the display is fine, the selection is empty". */
174
+ const EMPTY_SELECTION = [
175
+ // xclip 0.13, recorded under Xvfb with nothing on the PRIMARY selection.
176
+ /target \S+ not available/i,
177
+ // wl-clipboard's wording. DOCUMENTED, NOT RECORDED: the capture container
178
+ // had no compositor, so this branch is written from wl-clipboard's source
179
+ // and is the one string in this file nobody here has seen a program print.
180
+ /Nothing is copied/i,
181
+ ];
182
+ export function classifyClipboardRead(result) {
183
+ if (result.refused === true) {
184
+ return { outcome: "failed", reason: result.stderr };
185
+ }
186
+ if (result.missing) {
187
+ return unavailable("program", "the clipboard program for this platform is not installed on this host");
188
+ }
189
+ if (result.timedOut) {
190
+ // An X11 clipboard read against a wedged server BLOCKS rather than
191
+ // exiting, which is why every read here has a deadline. What it would
192
+ // have said is unknown, and unknown is not empty.
193
+ return unavailable("unknown", "the clipboard program did not finish within its timeout and was killed - the clipboard could not be read, which is not the same as it being empty");
194
+ }
195
+ if (result.code === 0) {
196
+ if (result.stdout.length > 0)
197
+ return { outcome: "read", text: result.stdout };
198
+ // Exit 0 and nothing on stdout: on macOS this is ambiguous and the caller
199
+ // runs the disambiguating probe; on Linux and Windows an empty successful
200
+ // read means an empty clipboard.
201
+ return { outcome: "empty", reason: "the clipboard program returned no bytes" };
202
+ }
203
+ for (const pattern of NO_SESSION) {
204
+ if (pattern.test(result.stderr)) {
205
+ return unavailable("session", `the clipboard backend could not reach a display: ${result.stderr.trim()}`);
206
+ }
207
+ }
208
+ for (const pattern of EMPTY_SELECTION) {
209
+ if (pattern.test(result.stderr)) {
210
+ return {
211
+ outcome: "empty",
212
+ reason: `the display was reachable and the selection holds nothing: ${result.stderr.trim()}`,
213
+ };
214
+ }
215
+ }
216
+ return {
217
+ outcome: "failed",
218
+ reason: `the clipboard program exited ${result.code}: ${result.stderr.trim()}`,
219
+ };
220
+ }
221
+ /**
222
+ * Read the macOS disambiguating probe.
223
+ *
224
+ * Empty output means the pasteboard genuinely holds nothing (recorded).
225
+ * Non-empty means it holds SOMETHING that `pbpaste` would not give us, which
226
+ * is a different answer and a different instruction to the caller.
227
+ */
228
+ export function classifyClipboardInfo(stdout) {
229
+ return stdout.trim() === "" ? "empty" : "noTextFlavour";
230
+ }
@@ -0,0 +1,220 @@
1
+ /**
2
+ * THE SECURITY BOUNDARY OF THIS PACKAGE.
3
+ *
4
+ * Every tool here carries a caller-supplied string to a platform API, and on
5
+ * two of the three platforms that API takes SOURCE CODE rather than data.
6
+ *
7
+ * macOS `osascript -e '<statement>'` compiles AppleScript. A title
8
+ * interpolated into `display notification "<title>"` is not text in
9
+ * a dialog — it is program text. A single double quote ends the
10
+ * string literal and everything after it RUNS, with the operator's
11
+ * automation permissions.
12
+ * Windows `powershell.exe -Command <script>` compiles PowerShell, and the
13
+ * toast payload inside it is an XML document. A body containing
14
+ * `</toast>` closes the document early; a body containing `&`
15
+ * makes it malformed. Same shape, one layer over.
16
+ * Linux `notify-send` takes argv, which is the easy case — except that a
17
+ * summary beginning with `-` is still read as a FLAG. Recorded on
18
+ * Debian 12 / libnotify 0.8.1: `notify-send "-u" "body"` prints
19
+ * "Unknown urgency body specified", because `-u` was consumed as
20
+ * the urgency flag and the body became its argument.
21
+ *
22
+ * THE THREE RULES THIS FILE ENFORCES.
23
+ *
24
+ * 1. NO SCRIPT SOURCE IS EVER BUILT AT CALL TIME. Every AppleScript and every
25
+ * PowerShell script in this package is a frozen module constant registered
26
+ * here at load. `osascriptArgv` and `powershellArgv` REFUSE a script object
27
+ * that is not in that registry, so "just interpolate it this once" cannot
28
+ * compile. This is stronger than escaping and needs no escaper to be
29
+ * correct: there is nothing to escape into.
30
+ * 2. CALLER VALUES TRAVEL BESIDE THE PROGRAM, NEVER INSIDE IT.
31
+ * - macOS: as ARGV, after `--`, read back with `item N of argv`. Verified
32
+ * on macOS 26.6.2 (Darwin 25.6.0) while writing this:
33
+ *
34
+ * $ osascript -e 'on run argv' \
35
+ * -e 'return (item 1 of argv) & "|" & (item 2 of argv)' \
36
+ * -e 'end run' -- '-e' 'say "hi"; display dialog "x"'
37
+ * -e|say "hi"; display dialog "x"
38
+ *
39
+ * Both hostile values came back as DATA. Nothing was spoken and no
40
+ * dialog appeared. Without the `--`, the same run answers
41
+ * "osascript: option requires an argument -- e", which is why the `--`
42
+ * is mandatory rather than decorative.
43
+ * - Windows: through the child's ENVIRONMENT. Argv is not a safe channel
44
+ * for PowerShell — `powershell -Command <script> a b c` CONCATENATES
45
+ * the trailing arguments onto the command string, so a value passed
46
+ * "as an argument" is still source.
47
+ * - Linux: as argv after `--`.
48
+ * 3. WHAT CANNOT TRAVEL BESIDE THE PROGRAM IS VALIDATED, NOT ESCAPED. The
49
+ * Windows toast body has to end up inside an XML document, so it is
50
+ * escaped here (`escapeXmlText`) and the FINISHED DOCUMENT is what crosses
51
+ * the environment channel — one testable escaper, applied once, whose
52
+ * output never becomes PowerShell source. Everything else that would have
53
+ * to be interpolated (a printer name, a page range) is matched against a
54
+ * closed pattern and refused if it does not fit.
55
+ */
56
+ /**
57
+ * One AppleScript, as a fixed list of statements.
58
+ *
59
+ * `lines` become one `-e` argument each. They are written by hand in this
60
+ * package and contain no caller value; `item N of argv` is how they reach
61
+ * what the caller sent.
62
+ */
63
+ export type AppleScript = {
64
+ readonly name: string;
65
+ readonly lines: readonly string[];
66
+ /** How many `argv` items the statements read. Checked when argv is built. */
67
+ readonly arity: number;
68
+ };
69
+ /** One PowerShell script, as a single statement string plus its env contract. */
70
+ export type PowerShellScript = {
71
+ readonly name: string;
72
+ readonly script: string;
73
+ /** Environment variable names the script reads. Nothing else is set. */
74
+ readonly reads: readonly string[];
75
+ };
76
+ /**
77
+ * Declare an AppleScript. Only a registered script may be run, and the only
78
+ * place `registerAppleScript` is called is at module scope in this package —
79
+ * so a script cannot be assembled from a caller's value and then run.
80
+ */
81
+ export declare function registerAppleScript(script: AppleScript): AppleScript;
82
+ export declare function registerPowerShellScript(script: PowerShellScript): PowerShellScript;
83
+ /**
84
+ * Every registered PowerShell script's SOURCE.
85
+ *
86
+ * Exported for one assertion: `every powershell argv is a registered script
87
+ * with nothing appended` in index.test.ts checks that the last element of
88
+ * every recorded `powershell.exe` argv is one of these, and that there is
89
+ * nothing after it. That narrow, checkable guarantee is what lets `run.ts`
90
+ * carve PowerShell out of its blanket shell refusal.
91
+ */
92
+ export declare function registeredPowerShellSources(): ReadonlySet<string>;
93
+ /** Raised when something tries to run source this file did not register. */
94
+ export declare class UnregisteredScriptError extends Error {
95
+ constructor(kind: string, name: string);
96
+ }
97
+ /**
98
+ * `osascript` with the caller's values as `argv`.
99
+ *
100
+ * The shape is `osascript -e 'on run argv' -e '<statement>' ... -e 'end run'
101
+ * -- <value> ...`, which is the whole point: the statements are compiled, the
102
+ * values are not. `--` is mandatory and is not decoration — see the header.
103
+ */
104
+ export declare function osascriptArgv(script: AppleScript, values?: readonly string[]): readonly string[];
105
+ /**
106
+ * The statements of an osascript argv — everything the compiler sees.
107
+ *
108
+ * Used by the tests that assert a hostile value never reaches program text,
109
+ * and by `dryRun` so a caller can read the source that would run.
110
+ */
111
+ export declare function osascriptProgramText(argv: readonly string[]): string;
112
+ /**
113
+ * An AppleScript error line, split into its code and message.
114
+ *
115
+ * Recorded on macOS 26.6.2 with Accessibility NOT granted:
116
+ * `44:87: execution error: System Events got an error: osascript is not
117
+ * allowed assistive access. (-25211)`
118
+ * The code is what distinguishes "you have not granted this" from "the script
119
+ * was wrong", and the two must not become the same result.
120
+ */
121
+ export declare function parseOsascriptError(stderr: string): {
122
+ code: number | null;
123
+ message: string;
124
+ };
125
+ /**
126
+ * macOS error codes that mean "the operator has not granted this", as
127
+ * distinct from "the script failed".
128
+ *
129
+ * -25211 and -1728 are both reported by System Events for an un-granted
130
+ * Accessibility request, which is why this is a set and not an equality
131
+ * check. -1743 is the Automation (Apple-events) denial, a DIFFERENT grant in
132
+ * a DIFFERENT pane of System Settings, so its message names the right one.
133
+ */
134
+ export declare const APPLESCRIPT_PERMISSION_CODES: ReadonlyMap<number, string>;
135
+ /**
136
+ * True when a failed osascript run failed for want of a PERMISSION.
137
+ *
138
+ * The message test is a fallback for a future OS that changes the code but
139
+ * keeps the sentence; a code match is preferred because a message is
140
+ * localised and this package pins `LC_ALL=C` for the commands it parses but
141
+ * cannot pin the language of a macOS framework string.
142
+ */
143
+ export declare function osascriptPermissionReason(stderr: string): string | undefined;
144
+ /** The fixed prefix for every PowerShell invocation this package makes. */
145
+ export declare const POWERSHELL_FLAGS: readonly string[];
146
+ /**
147
+ * `powershell.exe` running a registered script, with caller values in the
148
+ * ENVIRONMENT.
149
+ *
150
+ * Nothing is appended after the script: `powershell -Command <script> a b`
151
+ * concatenates `a b` onto the command string, which would put a caller's
152
+ * value back into source. The refusal below makes that impossible to write
153
+ * by accident — the env keys supplied must match the script's declared
154
+ * `reads` EXACTLY, so neither an unread extra nor a missing one gets through.
155
+ */
156
+ export declare function powershellArgv(script: PowerShellScript, env: Readonly<Record<string, string>>): {
157
+ readonly argv: readonly string[];
158
+ readonly env: Readonly<Record<string, string>>;
159
+ };
160
+ /**
161
+ * XML text escaping for the Windows toast payload.
162
+ *
163
+ * This is the one place in the package where a caller's value is put INSIDE
164
+ * something it could otherwise restructure, and it is done here — in
165
+ * TypeScript, where it can be tested against `</toast>`, `&`, `<`, `"`, a
166
+ * backslash and a newline — rather than in PowerShell, where the same
167
+ * escaping would be invisible to this suite.
168
+ *
169
+ * The finished document then crosses to PowerShell through the environment,
170
+ * so even the ESCAPED value is never part of the program text. Escaping is
171
+ * the second line of defence here, not the first.
172
+ *
173
+ * `'` and `"` are escaped although an XML text node does not require it: the
174
+ * value also has to survive an attribute position in a future template, and
175
+ * an escaper whose correctness depends on where its output is pasted is the
176
+ * kind that eventually is not.
177
+ */
178
+ export declare function escapeXmlText(value: string): string;
179
+ /**
180
+ * Strip what an XML 1.0 document cannot represent.
181
+ *
182
+ * A C0 control such as U+0001 has no escape in XML 1.0 — the numeric
183
+ * reference for it is itself illegal — so `LoadXml` throws on the document
184
+ * rather than showing a broken toast. Dropping it is the only option that
185
+ * still delivers the message, and the tool SAYS it did (`sanitized`) rather
186
+ * than silently changing what the operator asked to show. TAB, LF and CR are
187
+ * legal and are kept.
188
+ *
189
+ * U+007F (DEL) is in fact LEGAL in XML 1.0 — it is above U+0020 — and is
190
+ * dropped here anyway, because it is a discouraged character that no
191
+ * notification is improved by and its removal is reported like any other.
192
+ * Saying so is the point: an escaper whose comment overstates what it is
193
+ * doing is one somebody later "simplifies" on the strength of the comment.
194
+ */
195
+ export declare function stripXmlIllegalChars(value: string): {
196
+ text: string;
197
+ removed: number;
198
+ };
199
+ /**
200
+ * Would this value be read as an OPTION rather than as a value?
201
+ *
202
+ * `--` solves this wherever the program supports it. Verified on macOS
203
+ * 26.6.2: `open -- -a` answers "The file /private/tmp/-a does not exist",
204
+ * while `open -a` answers "open: option requires an argument -- a". Where a
205
+ * program documents no `--` — CUPS `lp` and `lpstat` do not — the answer is
206
+ * not to escape but to REFUSE, and to say so. This repo has already shipped
207
+ * one argument injection of exactly this shape (`gitBranchCreate({name:"-D"})`
208
+ * ran `git branch -D victim`).
209
+ */
210
+ export declare function looksLikeFlag(value: string): boolean;
211
+ /**
212
+ * A printer/queue name that can safely be an argv element.
213
+ *
214
+ * CUPS destination names are documented as printable ASCII without space,
215
+ * `/`, `#` or `@`; this is stricter still, and deliberately excludes a
216
+ * leading `-`.
217
+ */
218
+ export declare const SAFE_DESTINATION: RegExp;
219
+ /** A CUPS `page-ranges` value: `1`, `1-4`, `1,3,5-9`. Nothing else. */
220
+ export declare const SAFE_PAGE_RANGES: RegExp;