@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/dist/lib/open.js CHANGED
@@ -265,7 +265,24 @@ export function classifyTarget(target, allowed) {
265
265
  * - `.desktop`, `.lnk`, `.url` and `.webloc` are INDIRECTION. They contain a
266
266
  * target, and opening one reaches a program or a URL that never passed
267
267
  * the scheme allow-list at the top of this file. A `.url` file holding
268
- * `URL=file:///etc/passwd` walks straight around the gate.
268
+ * `URL=file:///etc/passwd` walks straight around the gate. The macOS
269
+ * location files are the same family: `.fileloc` is `.webloc`'s file:
270
+ * twin, and Launch Services resolves it straight to its target — an app
271
+ * outside the workspace, or a workspace `.command` this list refuses by
272
+ * name, laundered through one level of indirection (flag-truth-5#6,
273
+ * security-10#4). `.afploc`/`.ftploc` are schemes refused above.
274
+ *
275
+ * The list also names what INSTALLS or RUNS on open without an execute bit:
276
+ * configuration profiles, Java Web Start, ClickOnce and MSIX installers,
277
+ * scripts an interpreter's launcher is registered for, Automator and
278
+ * Shortcuts documents, macOS plug-in bundles, every disk image type macOS's
279
+ * DiskImageMounter registers, System Settings extensions and network
280
+ * configurations, certificates and keychains Keychain Access imports, and
281
+ * Microsoft's Level-1 attachment list in full. A deny-list always trails the
282
+ * handler table on a real machine; this one is the union of what the 0.7.0
283
+ * reviews found and those handlers register (read from their Info.plist
284
+ * files on macOS 26), and an allow-list of document types for path opens is
285
+ * the structural fix still to be decided.
269
286
  */
270
287
  const DANGEROUS_EXTENSIONS = new Set([
271
288
  // runs directly
@@ -293,6 +310,9 @@ const DANGEROUS_EXTENSIONS = new Set([
293
310
  "bash",
294
311
  "zsh",
295
312
  "fish",
313
+ "ksh",
314
+ "csh",
315
+ "tcsh",
296
316
  "command",
297
317
  "tool",
298
318
  "run",
@@ -307,17 +327,217 @@ const DANGEROUS_EXTENSIONS = new Set([
307
327
  "scptd",
308
328
  "applescript",
309
329
  "terminal",
330
+ // a Terminal.app session file: Terminal is its registered handler and
331
+ // runs the command its ExecutionString names
332
+ "term",
333
+ // more macOS bundles, installers and automation
334
+ "mpkg",
335
+ "prefpane",
336
+ "saver",
337
+ "service",
338
+ "osax",
339
+ "qlgenerator",
340
+ "kext",
341
+ "plugin",
342
+ "bundle",
343
+ "appex",
344
+ "action",
345
+ "caction",
346
+ "wflow",
347
+ "shortcut",
348
+ "mobileconfig",
349
+ "configprofile",
350
+ "provisionprofile",
351
+ // interpreters whose launcher is registered to RUN the file on open
352
+ "py",
353
+ "pyw",
354
+ "pyz",
355
+ "pyzw",
356
+ "pyc",
357
+ "pyo",
358
+ "pl",
359
+ "rb",
360
+ "jnlp",
361
+ // Windows: installers, shell and management documents that act on open
362
+ "appref-ms",
363
+ "application",
364
+ "appx",
365
+ "appxbundle",
366
+ "msix",
367
+ "msixbundle",
368
+ "appinstaller",
369
+ "msc",
370
+ "chm",
371
+ "gadget",
372
+ "ws",
373
+ "wsc",
374
+ "sct",
375
+ "diagcab",
376
+ "xll",
377
+ "rdp",
378
+ "theme",
379
+ "themepack",
380
+ "deskthemepack",
381
+ // the rest of Microsoft's Level-1 (always blocked) attachment list, in
382
+ // full: each runs, installs, configures or redirects on open
383
+ "appcontent-ms",
384
+ "printerexport",
385
+ "osd",
386
+ "prf",
387
+ "xnk",
388
+ "udl",
389
+ "vsmacros",
390
+ "vsw",
391
+ "webpnp",
392
+ "asp",
393
+ "aspx",
394
+ "asx",
395
+ "bgi",
396
+ "cnt",
397
+ "fxp",
398
+ "hpj",
399
+ "htc",
400
+ "its",
401
+ "ops",
402
+ "pcd",
403
+ "plg",
404
+ "prg",
405
+ "pst",
406
+ "tmp",
407
+ "msu",
408
+ "mst",
409
+ "cab",
410
+ "ins",
411
+ "isp",
412
+ "hlp",
413
+ "grp",
414
+ "mcf",
415
+ "xbap",
416
+ "vb",
417
+ "vbp",
418
+ "bas",
419
+ "shb",
420
+ "shs",
421
+ "wsb",
422
+ "msh",
423
+ "msh1",
424
+ "msh2",
425
+ "mshxml",
426
+ "msh1xml",
427
+ "msh2xml",
428
+ "ps1xml",
429
+ "ps2",
430
+ "ps2xml",
431
+ "psc1",
432
+ "psc2",
433
+ "psd1",
434
+ "psdm1",
435
+ // Access databases and their shortcuts run an AutoExec macro on open
436
+ "ade",
437
+ "adp",
438
+ "accda",
439
+ "accdb",
440
+ "accde",
441
+ "accdr",
442
+ "accdt",
443
+ "mad",
444
+ "maf",
445
+ "mag",
446
+ "mam",
447
+ "maq",
448
+ "mar",
449
+ "mas",
450
+ "mat",
451
+ "mau",
452
+ "mav",
453
+ "maw",
454
+ "mda",
455
+ "mdb",
456
+ "mde",
457
+ "mdt",
458
+ "mdw",
459
+ "mdz",
460
+ // Linux packages and self-running images
461
+ "deb",
462
+ "rpm",
463
+ "flatpakref",
464
+ "appimage",
465
+ "snap",
466
+ // disk images that mount on open (and skip mark-of-the-web on Windows):
467
+ // on macOS, every type DiskImageMounter registers
468
+ "iso",
469
+ "img",
470
+ "vhd",
471
+ "vhdx",
472
+ "udif",
473
+ "toast",
474
+ "dvdr",
475
+ "cdr",
476
+ "dmgpart",
477
+ "sparseimage",
478
+ "asif",
479
+ "sparsebundle",
480
+ "backupbundle",
481
+ // macOS System Settings extensions and network configurations, which it
482
+ // installs on open
483
+ "sysprefex",
484
+ "internetconnect",
485
+ "networkconnect",
486
+ // certificates and keychains: Keychain Access imports them on open (and
487
+ // Windows' Level-1 list names cer, crt and der for the same reason)
488
+ "cer",
489
+ "cert",
490
+ "crt",
491
+ "der",
492
+ "pem",
493
+ "p7",
494
+ "p7b",
495
+ "p7c",
496
+ "p7m",
497
+ "p7r",
498
+ "p7s",
499
+ "p10",
500
+ "p12",
501
+ "pfx",
502
+ "pkcs12",
503
+ "keychain",
504
+ "keychain-db",
310
505
  // indirection — the target never passes the scheme gate
311
506
  "desktop",
312
507
  "lnk",
313
508
  "url",
314
509
  "webloc",
315
510
  "inetloc",
511
+ "fileloc",
512
+ "vncloc",
513
+ "afploc",
514
+ "ftploc",
515
+ "mailloc",
516
+ "newsloc",
517
+ "atloc",
518
+ "telnetloc",
519
+ "website",
520
+ "scf",
521
+ "searchconnector-ms",
522
+ "library-ms",
523
+ "search-ms",
524
+ "settingcontent-ms",
316
525
  "inf",
317
526
  ]);
318
- /** The lowercase extension, or `""`. */
527
+ /**
528
+ * The lowercase extension, or `""`.
529
+ *
530
+ * Trailing dots and spaces are dropped first: Windows strips them when it
531
+ * opens a path, so `x.bat.` and `x.bat ` run `x.bat`, and reading them as
532
+ * "no extension" let a refused type through by spelling.
533
+ */
319
534
  export function extensionOf(path) {
320
- const base = path.split(/[\\/]/).pop() ?? path;
535
+ const full = path.split(/[\\/]/).pop() ?? path;
536
+ // A loop, not /[. ]+$/, which rescans the run from every start (quadratic).
537
+ let end = full.length;
538
+ while (end > 0 && (full[end - 1] === "." || full[end - 1] === " "))
539
+ end--;
540
+ const base = full.slice(0, end);
321
541
  const dot = base.lastIndexOf(".");
322
542
  return dot <= 0 ? "" : base.slice(dot + 1).toLowerCase();
323
543
  }
@@ -333,7 +553,7 @@ export function refusePath(absolutePath, facts) {
333
553
  }
334
554
  const extension = extensionOf(absolutePath);
335
555
  if (DANGEROUS_EXTENSIONS.has(extension)) {
336
- return `the path ends in ".${extension}", which a desktop either RUNS or follows to a target this tool never got to check — "open this" and "run this" are the same gesture to an operating system, so it is refused`;
556
+ return `the path ends in ".${extension}", which a desktop RUNS, installs, mounts or imports on open, or follows to a target this tool never got to check — "open this" and "run this" are the same gesture to an operating system, so it is refused`;
337
557
  }
338
558
  if (facts.isFile && isExecutable(facts)) {
339
559
  return "the path is an executable file, and asking a desktop to OPEN one is how it gets RUN — this tool refuses, whatever the registered handler would have done";
@@ -17,10 +17,13 @@
17
17
  * caller that cannot see when its own inhibitor lapses will hold another.
18
18
  * 2. **A PID IS NOT AN IDENTITY.** Pids are reused, and `release` reading a
19
19
  * stale state file would otherwise send SIGTERM to whatever now owns that
20
- * number. So the state file records the backend program, and `release` and
21
- * `status` VERIFY the live process's command line before signalling
22
- * anything. A mismatch is reported as `stale` and the file is cleared —
23
- * nothing is killed.
20
+ * number. So `release` and `status` VERIFY the live process's command line
21
+ * against the holder THIS package starts on this platform
22
+ * ({@link isHolderCommand}) before signalling anything — never against a
23
+ * value the record itself carries, since the record also supplies the pid.
24
+ * A mismatch is reported as `stale` and the file is cleared — nothing is
25
+ * killed. The record lives in a per-user private directory outside the
26
+ * workspace, where the agent's file tools cannot plant one.
24
27
  * 3. **WINDOWS CANNOT DO THIS IN A ONE-LINER.** `SetThreadExecutionState` is
25
28
  * THREAD-scoped and its effect dies with the thread, so a PowerShell
26
29
  * one-liner that sets the flag and exits does precisely nothing — the
@@ -39,8 +42,34 @@ export type AssertionScope = "system" | "display";
39
42
  /** The ceiling, and the reason there is one. Eight hours is a working day. */
40
43
  export declare const MAX_HOLD_MINUTES = 480;
41
44
  export declare const DEFAULT_HOLD_MINUTES = 30;
42
- /** Where the held assertion is remembered, relative to the workspace root. */
43
- export declare const STATE_RELATIVE_PATH = ".crewhaus/power-assertion.json";
45
+ /**
46
+ * Where 0.7.0 remembered the held assertion, relative to the workspace root.
47
+ *
48
+ * NOT READ ANY MORE, only reported. A file in the workspace can be written by
49
+ * the agent the tool serves (Write, CopyPath, an extracted archive), and the
50
+ * record names the pid `release` signals: a planted record made `release`
51
+ * SIGTERM any process of the operator's (security-10#1). The record now lives
52
+ * in a private per-user directory — see `powerStateFile` in ../index.ts.
53
+ */
54
+ export declare const LEGACY_STATE_RELATIVE_PATH = ".crewhaus/power-assertion.json";
55
+ /**
56
+ * The program this package's holder runs on `platform` — the ONE source for
57
+ * both `planHold` and the identity check `release` makes before signalling.
58
+ * The check used to compare against the `marker` field of the state record,
59
+ * which is the same record that supplies the pid, so whoever wrote the record
60
+ * chose what the live process was checked against (security-10#1).
61
+ */
62
+ export declare function holderMarker(platform: HostPlatform): string | undefined;
63
+ /**
64
+ * Is `commandLine` a holder THIS package starts on `platform`?
65
+ *
66
+ * The program must be the platform's holder, and the arguments must carry
67
+ * what `planHold` always gives it: `-t <seconds>` for caffeinate,
68
+ * `--who=crewhaus` for systemd-inhibit, and the generated type name inside a
69
+ * powershell host. An operator's own `caffeinate -d` is therefore never
70
+ * taken for ours.
71
+ */
72
+ export declare function isHolderCommand(platform: HostPlatform, commandLine: string): boolean;
44
73
  export declare const WINDOWS_INHIBIT: import("./escape").PowerShellScript;
45
74
  export declare const WINDOWS_PROCESS_PROBE: import("./escape").PowerShellScript;
46
75
  /**
@@ -134,7 +163,9 @@ export type Liveness = "alive" | "gone" | "reused" | "unknown";
134
163
  * test is kept for that case alone, narrowed to a powershell host.
135
164
  */
136
165
  export declare function commandMatchesMarker(commandLine: string, marker: string): boolean;
137
- export declare function classifyLiveness(marker: string, result: {
166
+ export declare function classifyLiveness(
167
+ /** The holder's program name, or a predicate over the live command line. */
168
+ holder: string | ((commandLine: string) => boolean), result: {
138
169
  readonly code: number;
139
170
  readonly stdout: string;
140
171
  readonly stderr: string;
package/dist/lib/power.js CHANGED
@@ -3,8 +3,62 @@ import { unsupportedPlatform } from "./outcome";
3
3
  /** The ceiling, and the reason there is one. Eight hours is a working day. */
4
4
  export const MAX_HOLD_MINUTES = 480;
5
5
  export const DEFAULT_HOLD_MINUTES = 30;
6
- /** Where the held assertion is remembered, relative to the workspace root. */
7
- export const STATE_RELATIVE_PATH = ".crewhaus/power-assertion.json";
6
+ /**
7
+ * Where 0.7.0 remembered the held assertion, relative to the workspace root.
8
+ *
9
+ * NOT READ ANY MORE, only reported. A file in the workspace can be written by
10
+ * the agent the tool serves (Write, CopyPath, an extracted archive), and the
11
+ * record names the pid `release` signals: a planted record made `release`
12
+ * SIGTERM any process of the operator's (security-10#1). The record now lives
13
+ * in a private per-user directory — see `powerStateFile` in ../index.ts.
14
+ */
15
+ export const LEGACY_STATE_RELATIVE_PATH = ".crewhaus/power-assertion.json";
16
+ /**
17
+ * The program this package's holder runs on `platform` — the ONE source for
18
+ * both `planHold` and the identity check `release` makes before signalling.
19
+ * The check used to compare against the `marker` field of the state record,
20
+ * which is the same record that supplies the pid, so whoever wrote the record
21
+ * chose what the live process was checked against (security-10#1).
22
+ */
23
+ export function holderMarker(platform) {
24
+ switch (platform) {
25
+ case "darwin":
26
+ return DARWIN_HOLDER;
27
+ case "linux":
28
+ return LINUX_HOLDER;
29
+ case "win32":
30
+ return WINDOWS_HOLDER_TYPE;
31
+ default:
32
+ return undefined;
33
+ }
34
+ }
35
+ const DARWIN_HOLDER = "caffeinate";
36
+ const LINUX_HOLDER = "systemd-inhibit";
37
+ /** The type name the Windows holder script defines; see WINDOWS_INHIBIT. */
38
+ const WINDOWS_HOLDER_TYPE = "CHPwr";
39
+ /**
40
+ * Is `commandLine` a holder THIS package starts on `platform`?
41
+ *
42
+ * The program must be the platform's holder, and the arguments must carry
43
+ * what `planHold` always gives it: `-t <seconds>` for caffeinate,
44
+ * `--who=crewhaus` for systemd-inhibit, and the generated type name inside a
45
+ * powershell host. An operator's own `caffeinate -d` is therefore never
46
+ * taken for ours.
47
+ */
48
+ export function isHolderCommand(platform, commandLine) {
49
+ const marker = holderMarker(platform);
50
+ if (marker === undefined || !commandMatchesMarker(commandLine, marker))
51
+ return false;
52
+ const tokens = commandLine.trim().split(/\s+/);
53
+ switch (platform) {
54
+ case "darwin":
55
+ return tokens.includes("-t");
56
+ case "linux":
57
+ return tokens.includes("--who=crewhaus");
58
+ default:
59
+ return true;
60
+ }
61
+ }
8
62
  export const WINDOWS_INHIBIT = registerPowerShellScript({
9
63
  name: "power-inhibit",
10
64
  // DOCUMENTED, NOT RECORDED. The flags are ES_CONTINUOUS (0x80000000) |
@@ -83,7 +137,7 @@ export function planHold(platform, scope, seconds, reason) {
83
137
  if (scope === "display")
84
138
  argv.push("-d");
85
139
  argv.push("-t", String(seconds));
86
- return { ok: true, backend: "caffeinate", marker: "caffeinate", request: { argv } };
140
+ return { ok: true, backend: "caffeinate", marker: DARWIN_HOLDER, request: { argv } };
87
141
  }
88
142
  case "linux": {
89
143
  const what = scope === "display" ? "idle:sleep:handle-lid-switch" : "idle:sleep";
@@ -101,7 +155,7 @@ export function planHold(platform, scope, seconds, reason) {
101
155
  return {
102
156
  ok: true,
103
157
  backend: "systemd-inhibit",
104
- marker: "systemd-inhibit",
158
+ marker: LINUX_HOLDER,
105
159
  request: { argv },
106
160
  };
107
161
  }
@@ -114,7 +168,7 @@ export function planHold(platform, scope, seconds, reason) {
114
168
  return {
115
169
  ok: true,
116
170
  backend: "powershell SetThreadExecutionState",
117
- marker: "CHPwr",
171
+ marker: WINDOWS_HOLDER_TYPE,
118
172
  request: { argv: built.argv, env: built.env },
119
173
  };
120
174
  }
@@ -184,7 +238,9 @@ export function commandMatchesMarker(commandLine, marker) {
184
238
  return true;
185
239
  return /^powershell$/i.test(base) && commandLine.includes(marker);
186
240
  }
187
- export function classifyLiveness(marker, result) {
241
+ export function classifyLiveness(
242
+ /** The holder's program name, or a predicate over the live command line. */
243
+ holder, result) {
188
244
  if (result.refused === true || result.missing || result.timedOut)
189
245
  return "unknown";
190
246
  // `ps -p <pid>` exits 1 with no output when nothing has that pid. That is a
@@ -208,7 +264,8 @@ export function classifyLiveness(marker, result) {
208
264
  const line = result.stdout.trim();
209
265
  if (line === "")
210
266
  return "gone";
211
- return commandMatchesMarker(line, marker) ? "alive" : "reused";
267
+ const ours = typeof holder === "string" ? commandMatchesMarker(line, holder) : holder(line);
268
+ return ours ? "alive" : "reused";
212
269
  }
213
270
  /** Minutes to a bounded second count, or a refusal naming the ceiling. */
214
271
  export function boundedSeconds(minutes) {
@@ -76,7 +76,12 @@ export const WINDOWS_PRESENCE = registerPowerShellScript({
76
76
  script: [
77
77
  `$src = '${WINDOWS_IDLE_SOURCE}'`,
78
78
  "try { Add-Type -TypeDefinition $src -ErrorAction Stop; [Console]::Out.WriteLine('idleMs=' + [CHIdle]::Ms()) } catch { [Console]::Out.WriteLine('idleMs=?') }",
79
- "$lock = @(Get-Process -Name LogonUI -ErrorAction SilentlyContinue).Count",
79
+ // THIS session's LogonUI only: Get-Process lists every session's, so on
80
+ // a host with another signed-in (or RDP) user, their lock screen read as
81
+ // this user being locked out — the direction that sends a workflow off
82
+ // to act unattended.
83
+ "$sid = [System.Diagnostics.Process]::GetCurrentProcess().SessionId",
84
+ "$lock = @(Get-Process -Name LogonUI -ErrorAction SilentlyContinue | Where-Object { $_.SessionId -eq $sid }).Count",
80
85
  "[Console]::Out.WriteLine('locked=' + $(if ($lock -gt 0) { 'yes' } else { 'no' }))",
81
86
  ].join("; "),
82
87
  reads: [],
@@ -218,6 +223,8 @@ export function parseLoginctl(stdout) {
218
223
  }
219
224
  /** Parse the two lines the Windows script prints. */
220
225
  export function parseWindowsPresence(stdout) {
226
+ // `[Console]::Out.WriteLine` ends each line with CRLF on Windows; `$`
227
+ // under `m` matches before `\r` as well as `\n`, so both parse.
221
228
  const idle = /^idleMs=(\d+)$/m.exec(stdout);
222
229
  const locked = /^locked=(yes|no)$/m.exec(stdout);
223
230
  return {
@@ -71,6 +71,14 @@ export type QueueFacts = {
71
71
  * parser that breaks on the next one.
72
72
  */
73
73
  export declare function parseQueues(stdout: string, stderr: string): QueueFacts;
74
+ /**
75
+ * Parse the Windows printer probe's JSON: `{"printers":[{"name","status"}],
76
+ * "default":<name>|null}`. Anything else — no stdout, a PowerShell error, a
77
+ * shape this parser does not know — is UNREADABLE, never "no printers".
78
+ * A name is taken whole, spaces, commas and a `\\server\queue` share
79
+ * included.
80
+ */
81
+ export declare function parseWindowsQueues(stdout: string, stderr: string): QueueFacts;
74
82
  /** The probe could not answer. Distinct from "it answered, there are none". */
75
83
  export declare function unreadableQueues(reason: string): QueueFacts;
76
84
  export type PrintOptions = {
package/dist/lib/print.js CHANGED
@@ -20,9 +20,17 @@ export function planPrinterProbe(platform) {
20
20
  }
21
21
  export const WINDOWS_PRINTER_LIST = registerPowerShellScript({
22
22
  name: "printer-list",
23
- // DOCUMENTED, NOT RECORDED — nobody here has a Windows box. `Get-Printer`
24
- // is in the PrintManagement module, present on client Windows since 8.1.
25
- script: "try { Get-Printer | ForEach-Object { [Console]::Out.WriteLine('printer ' + $_.Name + ' is ' + $_.PrinterStatus) } } catch { [Console]::Error.WriteLine($_.Exception.Message) }",
23
+ // DOCUMENTED, NOT RECORDED on a desktop — `Get-Printer` is in the
24
+ // PrintManagement module, present on client Windows since 8.1; the
25
+ // windows-tools CI job logs what a Windows runner prints (see
26
+ // windows-recording.test.ts). JSON, not prose: the 0.7.0 script printed
27
+ // `printer <Name> is <Status>` into the lpstat parser, whose `\S+` cut every
28
+ // Windows name with a space at the first one — every Windows box ships
29
+ // "Microsoft Print to PDF" (reliability#7). Names and statuses are cast to
30
+ // strings (an enum would serialise as a number), the list is forced to an
31
+ // array (ConvertTo-Json unwraps a one-element pipeline), and a failure goes
32
+ // to stderr with nothing on stdout, so it can never read as "no printers".
33
+ script: "try { $rows = @(Get-Printer -ErrorAction Stop | ForEach-Object { [pscustomobject]@{ name = [string]$_.Name; status = [string]$_.PrinterStatus } }); $def = $null; try { $def = [string](Get-CimInstance -ClassName Win32_Printer -Filter 'Default=TRUE' -ErrorAction Stop | Select-Object -First 1 -ExpandProperty Name) } catch { $def = $null }; [Console]::Out.Write((ConvertTo-Json -InputObject ([pscustomobject]@{ printers = $rows; default = $def }) -Compress -Depth 3)) } catch { [Console]::Error.WriteLine($_.Exception.Message) }",
26
34
  reads: [],
27
35
  });
28
36
  export const WINDOWS_PRINT_TEXT = registerPowerShellScript({
@@ -75,6 +83,58 @@ export function parseQueues(stdout, stderr) {
75
83
  }
76
84
  return { printers, defaultPrinter, schedulerDown: false, unreadable: null };
77
85
  }
86
+ /**
87
+ * Parse the Windows printer probe's JSON: `{"printers":[{"name","status"}],
88
+ * "default":<name>|null}`. Anything else — no stdout, a PowerShell error, a
89
+ * shape this parser does not know — is UNREADABLE, never "no printers".
90
+ * A name is taken whole, spaces, commas and a `\\server\queue` share
91
+ * included.
92
+ */
93
+ export function parseWindowsQueues(stdout, stderr) {
94
+ const text = stdout.trim();
95
+ const why = stderr.trim();
96
+ const unreadable = (detail) => unreadableQueues(`the Windows printer probe ${detail}${why === "" ? "" : `: ${why.slice(0, 300)}`}, so whether this host has printers is unknown — it is NOT that it has none`);
97
+ if (text === "")
98
+ return unreadable("printed nothing");
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(text);
102
+ }
103
+ catch {
104
+ return unreadable("did not print the JSON it was written to print");
105
+ }
106
+ if (typeof parsed !== "object" || parsed === null)
107
+ return unreadable("printed an unknown shape");
108
+ const record = parsed;
109
+ const rawList = record["printers"];
110
+ // ConvertTo-Json unwraps a one-element array under some hosts; accept both.
111
+ const list = Array.isArray(rawList)
112
+ ? rawList
113
+ : typeof rawList === "object" && rawList !== null
114
+ ? [rawList]
115
+ : rawList === null || rawList === undefined
116
+ ? []
117
+ : undefined;
118
+ if (list === undefined)
119
+ return unreadable("printed an unknown shape");
120
+ const printers = [];
121
+ for (const row of list) {
122
+ if (typeof row !== "object" || row === null)
123
+ return unreadable("printed an unknown row");
124
+ const name = row["name"];
125
+ const status = row["status"];
126
+ if (typeof name !== "string" || name === "")
127
+ return unreadable("printed a row with no name");
128
+ printers.push({ name, state: typeof status === "string" ? status : "" });
129
+ }
130
+ const dflt = record["default"];
131
+ return {
132
+ printers,
133
+ defaultPrinter: typeof dflt === "string" && dflt !== "" ? dflt : null,
134
+ schedulerDown: false,
135
+ unreadable: null,
136
+ };
137
+ }
78
138
  /** The probe could not answer. Distinct from "it answered, there are none". */
79
139
  export function unreadableQueues(reason) {
80
140
  return { printers: [], defaultPrinter: null, schedulerDown: false, unreadable: reason };
@@ -95,8 +95,11 @@ export const WINDOWS_WINDOW_LIST = registerPowerShellScript({
95
95
  // window per process and no geometry; real bounds need EnumWindows +
96
96
  // GetWindowRect through a P/Invoke shim, which is a compile step this
97
97
  // package will not take (house rule 8). So the Windows rows report bounds
98
- // as UNKNOWN rather than as zeros, and say why.
99
- script: "$fs = [char]31; $rs = [char]30; Get-Process | Where-Object { $_.MainWindowHandle -ne 0 } | ForEach-Object { [Console]::Out.Write($_.ProcessName + $fs + $_.MainWindowTitle + $fs + '?' + $fs + '?' + $fs + '?' + $fs + '?' + $fs + 'false' + $fs + '?' + $rs) }",
98
+ // as UNKNOWN rather than as zeros, and say why. Focus too: the 0.7.0 script
99
+ // printed a literal `false` for every window, a definite "nothing has
100
+ // focus" from a probe that never asked (GetForegroundWindow is the same
101
+ // P/Invoke step), so focused is `?` → null like the bounds.
102
+ script: "$fs = [char]31; $rs = [char]30; Get-Process | Where-Object { $_.MainWindowHandle -ne 0 } | ForEach-Object { [Console]::Out.Write($_.ProcessName + $fs + $_.MainWindowTitle + $fs + '?' + $fs + '?' + $fs + '?' + $fs + '?' + $fs + '?' + $fs + '?' + $rs) }",
100
103
  reads: [],
101
104
  });
102
105
  export function planWindowList(platform, env) {
package/dist/paths.d.ts CHANGED
@@ -1,10 +1,29 @@
1
1
  import { CrewhausError } from "@crewhaus/errors";
2
- /** Raised when a caller-supplied path resolves outside the workspace root. */
2
+ /**
3
+ * The longest path this resolves, in UTF-16 code units; a longer one is
4
+ * refused before it is walked. Nothing that long can be opened or created:
5
+ * PATH_MAX is 1,024 bytes on macOS and 4,096 on Linux, Windows stops at
6
+ * 32,767 UTF-16 units, and a path's UTF-8 form is never shorter than its
7
+ * UTF-16 one. The limit is twice that because the part of a path that exists
8
+ * is itself under PATH_MAX, so the part that does not is over PATH_MAX on its
9
+ * own: no spelling of the place reaches it, not even one through a symlink
10
+ * that shortens the part that exists.
11
+ */
12
+ export declare const MAX_PATH_CHARS: number;
13
+ /**
14
+ * Raised when a caller-supplied path resolves outside the workspace root, or
15
+ * is longer than any path can be.
16
+ */
3
17
  export declare class ToolPermissionError extends CrewhausError {
4
18
  readonly name = "ToolPermissionError";
5
19
  readonly toolName: string;
6
20
  readonly path: string;
7
- constructor(toolName: string, attemptedPath: string);
21
+ /**
22
+ * Why, when it is not that the location escapes the workspace root: a path
23
+ * longer than any path can be. A caller that rewords the refusal says this.
24
+ */
25
+ readonly reason: string | undefined;
26
+ constructor(toolName: string, attemptedPath: string, reason?: string);
8
27
  }
9
28
  /**
10
29
  * A validated path, in both forms the tools need.