@crewhaus/tool-desktop 0.7.0 → 0.8.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/LICENSE +201 -0
- package/NOTICE +9 -0
- package/README.md +121 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +139 -19
- package/dist/lib/clipboard.d.ts +1 -0
- package/dist/lib/clipboard.js +6 -0
- package/dist/lib/fsseam.d.ts +12 -0
- package/dist/lib/fsseam.js +86 -11
- package/dist/lib/open.d.ts +7 -1
- package/dist/lib/open.js +224 -4
- package/dist/lib/power.d.ts +38 -7
- package/dist/lib/power.js +64 -7
- package/dist/lib/presence.js +8 -1
- package/dist/lib/print.d.ts +8 -0
- package/dist/lib/print.js +63 -3
- package/dist/lib/windows.js +5 -2
- package/dist/paths.d.ts +21 -2
- package/dist/paths.js +72 -13
- package/dist/run.d.ts +7 -0
- package/dist/run.js +55 -75
- package/package.json +12 -7
- package/dist/fixtures.d.ts +0 -84
- package/dist/fixtures.js +0 -130
package/dist/paths.js
CHANGED
|
@@ -11,15 +11,45 @@
|
|
|
11
11
|
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { CrewhausError } from "@crewhaus/errors";
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The longest path this resolves, in UTF-16 code units; a longer one is
|
|
16
|
+
* refused before it is walked. Nothing that long can be opened or created:
|
|
17
|
+
* PATH_MAX is 1,024 bytes on macOS and 4,096 on Linux, Windows stops at
|
|
18
|
+
* 32,767 UTF-16 units, and a path's UTF-8 form is never shorter than its
|
|
19
|
+
* UTF-16 one. The limit is twice that because the part of a path that exists
|
|
20
|
+
* is itself under PATH_MAX, so the part that does not is over PATH_MAX on its
|
|
21
|
+
* own: no spelling of the place reaches it, not even one through a symlink
|
|
22
|
+
* that shortens the part that exists.
|
|
23
|
+
*/
|
|
24
|
+
export const MAX_PATH_CHARS = 2 * (process.platform === "darwin" ? 1_024 : process.platform === "win32" ? 32_767 : 4_096);
|
|
25
|
+
/** A path as a refusal quotes it: whole, unless it is past {@link MAX_PATH_CHARS}. */
|
|
26
|
+
function shownPath(given) {
|
|
27
|
+
return given.length > MAX_PATH_CHARS ? `${given.slice(0, 64)}…` : given;
|
|
28
|
+
}
|
|
29
|
+
/** Why a path past {@link MAX_PATH_CHARS} is refused: its length and the limit. */
|
|
30
|
+
function tooLong(abs) {
|
|
31
|
+
return `it is ${abs.length} characters once resolved, over the ${MAX_PATH_CHARS}-character limit: no path that long can be opened or created`;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Raised when a caller-supplied path resolves outside the workspace root, or
|
|
35
|
+
* is longer than any path can be.
|
|
36
|
+
*/
|
|
15
37
|
export class ToolPermissionError extends CrewhausError {
|
|
16
38
|
name = "ToolPermissionError";
|
|
17
39
|
toolName;
|
|
18
40
|
path;
|
|
19
|
-
|
|
20
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Why, when it is not that the location escapes the workspace root: a path
|
|
43
|
+
* longer than any path can be. A caller that rewords the refusal says this.
|
|
44
|
+
*/
|
|
45
|
+
reason;
|
|
46
|
+
constructor(toolName, attemptedPath, reason) {
|
|
47
|
+
super("tool", reason === undefined
|
|
48
|
+
? `tool "${toolName}" rejected path "${attemptedPath}": resolved location escapes the workspace root`
|
|
49
|
+
: `tool "${toolName}" rejected path "${shownPath(attemptedPath)}": ${reason}`);
|
|
21
50
|
this.toolName = toolName;
|
|
22
51
|
this.path = attemptedPath;
|
|
52
|
+
this.reason = reason;
|
|
23
53
|
}
|
|
24
54
|
}
|
|
25
55
|
/** The workspace root, resolved. Every path is contained within it. */
|
|
@@ -67,15 +97,7 @@ function nameExists(p) {
|
|
|
67
97
|
function resolveLocation(target, depth = 0) {
|
|
68
98
|
if (depth > 40)
|
|
69
99
|
throw new Error(`symlink chain at "${target}" is too long to resolve`);
|
|
70
|
-
|
|
71
|
-
const tail = [];
|
|
72
|
-
while (!nameExists(probe)) {
|
|
73
|
-
tail.unshift(path.basename(probe));
|
|
74
|
-
const parent = path.dirname(probe);
|
|
75
|
-
if (parent === probe)
|
|
76
|
-
break; // reached the filesystem root
|
|
77
|
-
probe = parent;
|
|
78
|
-
}
|
|
100
|
+
const { probe, tail } = deepestName(target);
|
|
79
101
|
let probeReal;
|
|
80
102
|
try {
|
|
81
103
|
probeReal = realpathSync(probe);
|
|
@@ -97,7 +119,41 @@ function resolveLocation(target, depth = 0) {
|
|
|
97
119
|
const base = realpathSync(path.dirname(probe));
|
|
98
120
|
probeReal = resolveLocation(path.resolve(base, link), depth + 1);
|
|
99
121
|
}
|
|
100
|
-
|
|
122
|
+
// One string, not one argument per name: Bun's `path.join` keeps only
|
|
123
|
+
// (argument count mod 65,536) of its arguments.
|
|
124
|
+
return tail.length > 0 ? path.join(probeReal, tail) : probeReal;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* `target`, an absolute path, split where its deepest prefix that exists as
|
|
128
|
+
* a NAME ends: that prefix, and the names below it, which do not exist, as
|
|
129
|
+
* one string ("" when `target` itself exists). This is `path.dirname` and
|
|
130
|
+
* `path.basename` worked by index on `target` itself: handing them each
|
|
131
|
+
* shorter prefix in turn copied the rest of the path once per missing name,
|
|
132
|
+
* the square of a count the caller picks.
|
|
133
|
+
*/
|
|
134
|
+
function deepestName(target) {
|
|
135
|
+
const top = path.parse(target).root.length;
|
|
136
|
+
const isSep = (i) => target[i] === "/" || target[i] === path.sep;
|
|
137
|
+
let probe = target;
|
|
138
|
+
let start = target.length;
|
|
139
|
+
let stop = -1;
|
|
140
|
+
while (!nameExists(probe)) {
|
|
141
|
+
// `path.basename(probe)` is `target.slice(start, end)`, and
|
|
142
|
+
// `path.dirname(probe)` is `target.slice(0, up)`.
|
|
143
|
+
let end = probe.length;
|
|
144
|
+
while (end > top && isSep(end - 1))
|
|
145
|
+
end--;
|
|
146
|
+
if (stop < 0)
|
|
147
|
+
stop = end;
|
|
148
|
+
start = end;
|
|
149
|
+
while (start > top && !isSep(start - 1))
|
|
150
|
+
start--;
|
|
151
|
+
const up = start > top ? start - 1 : top;
|
|
152
|
+
if (up === probe.length)
|
|
153
|
+
break; // reached the filesystem root
|
|
154
|
+
probe = target.slice(0, up);
|
|
155
|
+
}
|
|
156
|
+
return { probe, tail: stop < 0 ? "" : target.slice(start, stop) };
|
|
101
157
|
}
|
|
102
158
|
export function resolveSafe(toolName, rel, root = workspaceRoot()) {
|
|
103
159
|
const rootResolved = path.resolve(root);
|
|
@@ -107,6 +163,9 @@ export function resolveSafe(toolName, rel, root = workspaceRoot()) {
|
|
|
107
163
|
if (abs !== rootResolved && !abs.startsWith(`${rootResolved}${path.sep}`)) {
|
|
108
164
|
throw new ToolPermissionError(toolName, rel);
|
|
109
165
|
}
|
|
166
|
+
// Longer than any path can be: refused before the walk below reads it.
|
|
167
|
+
if (abs.length > MAX_PATH_CHARS)
|
|
168
|
+
throw new ToolPermissionError(toolName, rel, tooLong(abs));
|
|
110
169
|
let real;
|
|
111
170
|
try {
|
|
112
171
|
const rootReal = realpathSync(rootResolved);
|
package/dist/run.d.ts
CHANGED
|
@@ -73,6 +73,13 @@ export type RunResult = {
|
|
|
73
73
|
*/
|
|
74
74
|
readonly stdoutTruncated?: boolean;
|
|
75
75
|
readonly stderrTruncated?: boolean;
|
|
76
|
+
/**
|
|
77
|
+
* The output could not be read to its end: the child exited but something
|
|
78
|
+
* it started kept the pipe open past the drain grace. What arrived is
|
|
79
|
+
* returned, and `stdoutTruncated` is set too. 0.7.0 replaced such output
|
|
80
|
+
* with "", a definite-looking empty answer built from nothing.
|
|
81
|
+
*/
|
|
82
|
+
readonly outputIncomplete?: boolean;
|
|
76
83
|
/** Set when no runner was installed and the real host was not allowed. */
|
|
77
84
|
readonly refused?: boolean;
|
|
78
85
|
};
|
package/dist/run.js
CHANGED
|
@@ -45,6 +45,8 @@
|
|
|
45
45
|
* every use is inside a function body, and breaks the first time either file
|
|
46
46
|
* grows a module-scope constant that reads the other.
|
|
47
47
|
*/
|
|
48
|
+
import { constants as osConstants } from "node:os";
|
|
49
|
+
import { spawnBounded } from "@crewhaus/tool-safety/streams";
|
|
48
50
|
/** `PATH` for a child, with a floor so a stripped harness env still spawns. */
|
|
49
51
|
export const FALLBACK_PATH = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
50
52
|
/** Grace between the deadline's SIGTERM and the SIGKILL that follows it. */
|
|
@@ -349,87 +351,65 @@ export async function runHostCommand(request) {
|
|
|
349
351
|
refused: true,
|
|
350
352
|
};
|
|
351
353
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
354
|
+
// Output is capped AS IT ARRIVES (tool-safety's spawnBounded): at most
|
|
355
|
+
// three bytes a character of the cap are kept per stream (no UTF-16 unit
|
|
356
|
+
// takes more), and the rest is drained unstored, so the child never
|
|
357
|
+
// blocks on a full pipe and its exit code stays truthful. 0.7.0 read each
|
|
358
|
+
// pipe to EOF into memory and cut it afterwards, so memory was bounded by
|
|
359
|
+
// the child's output, not by the cap: 100 MiB of output with a
|
|
360
|
+
// 1000-character cap grew the process by about 311 MiB (C163).
|
|
361
|
+
//
|
|
362
|
+
// The drain bound is unchanged in intent: `xclip -i` and `wl-copy` leave
|
|
363
|
+
// a holder with the pipe open, and after `DRAIN_GRACE_MS` reading stops.
|
|
364
|
+
// A holder that exits normally is never killed (it IS the selection);
|
|
365
|
+
// the group is killed only at the deadline or on abort.
|
|
366
|
+
const cap = request.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
367
|
+
const run = await spawnBounded({
|
|
368
|
+
cmd: request.argv,
|
|
369
|
+
env: childEnv(request.env),
|
|
370
|
+
...(request.stdin === undefined ? {} : { stdin: request.stdin }),
|
|
371
|
+
timeoutMs: request.timeoutMs,
|
|
372
|
+
maxStdoutBytes: cap * 3,
|
|
373
|
+
maxStderrBytes: cap * 3,
|
|
374
|
+
killGraceMs: KILL_GRACE_MS,
|
|
375
|
+
drainGraceMs: DRAIN_GRACE_MS,
|
|
376
|
+
...(request.signal !== undefined ? { signal: request.signal } : {}),
|
|
377
|
+
});
|
|
378
|
+
if (run.spawnError !== undefined) {
|
|
363
379
|
// ENOENT means the backend is not installed on this host, which is a
|
|
364
380
|
// DIFFERENT answer from "it ran and told us nothing" — and a different
|
|
365
381
|
// answer again from "it is there and would not start". See
|
|
366
382
|
// `classifySpawnError`.
|
|
383
|
+
const err = new Error(run.spawnError);
|
|
384
|
+
if (run.spawnErrorCode !== undefined)
|
|
385
|
+
err.code = run.spawnErrorCode;
|
|
367
386
|
return classifySpawnError(err);
|
|
368
387
|
}
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
.catch(() => "");
|
|
395
|
-
const stderrText = new Response(proc.stderr)
|
|
396
|
-
.text()
|
|
397
|
-
.catch(() => "");
|
|
398
|
-
const code = await proc.exited;
|
|
399
|
-
let drainTimer;
|
|
400
|
-
const drainFallback = new Promise((resolve) => {
|
|
401
|
-
drainTimer = setTimeout(() => resolve(""), DRAIN_GRACE_MS);
|
|
402
|
-
});
|
|
403
|
-
try {
|
|
404
|
-
const [rawOut, rawErr] = await Promise.all([
|
|
405
|
-
Promise.race([stdoutText, drainFallback]),
|
|
406
|
-
Promise.race([stderrText, drainFallback]),
|
|
407
|
-
]);
|
|
408
|
-
const cap = request.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
|
|
409
|
-
const out = capText(rawOut, cap);
|
|
410
|
-
const err = capText(rawErr, cap);
|
|
411
|
-
return {
|
|
412
|
-
code,
|
|
413
|
-
stdout: out.text,
|
|
414
|
-
stderr: err.text,
|
|
415
|
-
timedOut,
|
|
416
|
-
missing: false,
|
|
417
|
-
...(out.truncated ? { stdoutTruncated: true } : {}),
|
|
418
|
-
...(err.truncated ? { stderrTruncated: true } : {}),
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
finally {
|
|
422
|
-
// One shared timer, cleared as soon as the race settles: a fresh
|
|
423
|
-
// `setTimeout` per stream that nobody cancels keeps the event loop
|
|
424
|
-
// awake for the full grace period after every spawn.
|
|
425
|
-
if (drainTimer !== undefined)
|
|
426
|
-
clearTimeout(drainTimer);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
finally {
|
|
430
|
-
clearTimeout(term);
|
|
431
|
-
clearTimeout(hardKill);
|
|
432
|
-
}
|
|
388
|
+
const out = capText(run.stdout, cap);
|
|
389
|
+
const err = capText(run.stderr, cap);
|
|
390
|
+
// Output something the child started kept open past the drain grace is
|
|
391
|
+
// what ARRIVED, not necessarily all of it: it is returned, flagged, and
|
|
392
|
+
// never presented as a complete answer. 0.7.0 replaced it with "".
|
|
393
|
+
const incomplete = !run.outputComplete;
|
|
394
|
+
return {
|
|
395
|
+
code: exitCodeOf(run.exitCode, run.signal),
|
|
396
|
+
stdout: out.text,
|
|
397
|
+
stderr: err.text,
|
|
398
|
+
timedOut: run.timedOut,
|
|
399
|
+
missing: false,
|
|
400
|
+
...(out.truncated || run.stdoutTruncated || incomplete ? { stdoutTruncated: true } : {}),
|
|
401
|
+
...(err.truncated || run.stderrTruncated ? { stderrTruncated: true } : {}),
|
|
402
|
+
...(incomplete ? { outputIncomplete: true } : {}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
/** The code `Bun.spawn`'s `exited` gave: the exit status, or 128 + the signal's number. */
|
|
406
|
+
function exitCodeOf(exitCode, signal) {
|
|
407
|
+
if (exitCode !== null)
|
|
408
|
+
return exitCode;
|
|
409
|
+
const number = signal === null
|
|
410
|
+
? undefined
|
|
411
|
+
: osConstants.signals[signal];
|
|
412
|
+
return number === undefined ? -1 : 128 + number;
|
|
433
413
|
}
|
|
434
414
|
/**
|
|
435
415
|
* Start a child and return its pid without waiting for it.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/tool-desktop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Deterministic access to the operator's desktop: clipboard, notifications, opening a document, printing, windows, presence and sleep — failing closed on a headless host",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -8,17 +8,19 @@
|
|
|
8
8
|
"exports": {
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./dist/index.d.ts",
|
|
11
|
-
"import": "./dist/index.js"
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"scripts": {
|
|
15
16
|
"test": "bun test src"
|
|
16
17
|
},
|
|
17
18
|
"dependencies": {
|
|
18
|
-
"@crewhaus/errors": "0.
|
|
19
|
-
"@crewhaus/tool-builder": "0.
|
|
20
|
-
"@crewhaus/tool-catalog": "0.
|
|
21
|
-
"@crewhaus/tool-proc": "0.
|
|
19
|
+
"@crewhaus/errors": "0.8.0",
|
|
20
|
+
"@crewhaus/tool-builder": "0.8.0",
|
|
21
|
+
"@crewhaus/tool-catalog": "0.8.0",
|
|
22
|
+
"@crewhaus/tool-proc": "0.8.0",
|
|
23
|
+
"@crewhaus/tool-safety": "0.8.0",
|
|
22
24
|
"zod": "^3.23.8"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
@@ -42,5 +44,8 @@
|
|
|
42
44
|
"publishConfig": {
|
|
43
45
|
"access": "public"
|
|
44
46
|
},
|
|
45
|
-
"files": ["dist", "README.md", "LICENSE", "NOTICE"]
|
|
47
|
+
"files": ["dist", "README.md", "LICENSE", "NOTICE"],
|
|
48
|
+
"engines": {
|
|
49
|
+
"bun": ">=1.3.14"
|
|
50
|
+
}
|
|
46
51
|
}
|
package/dist/fixtures.d.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
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
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
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";
|