unity-mcp-cli 0.70.0 → 0.71.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,159 @@
1
+ /**
2
+ * Supported `process.platform` values for the launch-errors dialog
3
+ * dismiss helper. Narrowed alias of NodeJS.Platform — keeps the
4
+ * platform-dispatch table exhaustive in tests without forcing callers
5
+ * to import a Node-internal type.
6
+ */
7
+ export type DismissPlatform = 'win32' | 'darwin' | 'linux';
8
+ /**
9
+ * Outcome of a single dismiss attempt against the running OS desktop.
10
+ *
11
+ * `dismissed`: the dialog was found AND a click was dispatched
12
+ * successfully. The polling loop should stop and return.
13
+ *
14
+ * `not-found`: no matching dialog was visible on this poll tick. The
15
+ * polling loop should continue ticking until either the overall
16
+ * timeout elapses or Unity reports ready (the existing wait-for-ready
17
+ * logic, which runs in parallel, is the authoritative ready signal).
18
+ *
19
+ * `error`: an unexpected platform error happened (a required tool was
20
+ * missing, a syscall failed). The polling loop logs the error once
21
+ * and continues with `not-found` semantics — the dialog may simply
22
+ * not be open yet, and a single transient error must not abort the
23
+ * whole launch flow.
24
+ */
25
+ export type DismissOutcome = {
26
+ kind: 'dismissed';
27
+ button: string;
28
+ } | {
29
+ kind: 'not-found';
30
+ } | {
31
+ kind: 'error';
32
+ message: string;
33
+ };
34
+ /**
35
+ * Window-title fragments matched against the Unity launch-errors
36
+ * dialog. Both legacy and current strings are listed so the matcher
37
+ * stays resilient across Unity versions. The match is case-insensitive
38
+ * and substring-based — Unity's actual title varies by version but
39
+ * always contains one of these.
40
+ *
41
+ * Exposed so tests can assert the matcher knows about both spellings
42
+ * without grepping the implementation.
43
+ */
44
+ export declare const LAUNCH_ERROR_DIALOG_TITLE_FRAGMENTS: readonly string[];
45
+ /** The button label this helper presses to dismiss the dialog. */
46
+ export declare const DISMISS_BUTTON_LABEL = "Ignore";
47
+ /**
48
+ * Producer-side prefixes for error messages that callers treat as
49
+ * permanent (the polling loop bails out instead of ticking again).
50
+ *
51
+ * Exported so the bailout matcher in `lib/open.ts` and the
52
+ * error-construction sites below reference the SAME literal — a
53
+ * future re-word lands in both places at once.
54
+ *
55
+ * The matcher in `lib/open.ts` uses `String.includes`, so each
56
+ * constant just needs to be a stable substring of the full message.
57
+ */
58
+ export declare const LINUX_XDOTOOL_MISSING_PREFIX = "xdotool not found on PATH";
59
+ export declare const UNSUPPORTED_PLATFORM_PREFIX = "Unsupported platform for launch-errors auto-dismiss";
60
+ /**
61
+ * Try once to find and dismiss the Unity launch-errors dialog on the
62
+ * current OS desktop. Pure-ish — performs a single OS call and
63
+ * returns; never blocks past the underlying syscall's own timeout.
64
+ *
65
+ * Library-safe: never throws (errors are returned in the
66
+ * `DismissOutcome` union), never writes to stdout/stderr, never
67
+ * mutates global state.
68
+ *
69
+ * The helper is platform-dispatched:
70
+ * - **Windows**: Win32 (`FindWindowW` / `EnumWindows` /
71
+ * `EnumChildWindows` / `GetWindowTextW` / `SendMessageW(BM_CLICK)`)
72
+ * driven from PowerShell so we do not pull in a native node-gyp
73
+ * dependency. UI Automation is the documented fallback if title
74
+ * matching breaks on a future Unity release.
75
+ * - **macOS**: AppleScript via `osascript` (the leaner
76
+ * AX-C-API-direct path is a documented follow-up). Requires the
77
+ * user to have granted Accessibility permission to the terminal /
78
+ * `unity-mcp-cli` binary once.
79
+ * - **Linux/X11**: `xdotool` (documented as a Linux platform
80
+ * dependency; `wmctrl` is acceptable as an alternative window
81
+ * enumerator). Wayland is deferred — call out explicitly in the
82
+ * error message so the user does not waste time debugging.
83
+ */
84
+ export declare function tryDismissLaunchErrorsDialog(platform?: DismissPlatform): Promise<DismissOutcome>;
85
+ /**
86
+ * The PowerShell payload that probes for the Unity launch-errors
87
+ * dialog and clicks `Ignore` if found. Exported as a string (not a
88
+ * function) so tests can assert the script shape without launching
89
+ * PowerShell.
90
+ *
91
+ * Strategy:
92
+ * 1. P/Invoke `EnumWindows` via Add-Type to enumerate every visible
93
+ * top-level window owned by `Unity.exe`.
94
+ * 2. Filter by title fragment (case-insensitive substring).
95
+ * 3. Walk child windows with `EnumChildWindows`, looking for a
96
+ * Button whose text equals `Ignore`.
97
+ * 4. Send `BM_CLICK` (0x00F5) to the matched button. `BM_CLICK` is
98
+ * preferred over a synthesised mouse event — it works even if
99
+ * the user is mid-click in another app, and it does not steal
100
+ * focus.
101
+ *
102
+ * The script writes a single-token result to stdout:
103
+ * - `dismissed:<button>` on success
104
+ * - `not-found` when no dialog was matched
105
+ * - `error:<message>` on an unexpected exception
106
+ */
107
+ export declare const WINDOWS_DISMISS_PS_SCRIPT: string;
108
+ /**
109
+ * The AppleScript snippet used for the macOS dismiss path. Exposed as
110
+ * a string for testability.
111
+ *
112
+ * Strategy: iterate every window of the Unity application process and
113
+ * click the first button titled `Ignore`. If the Unity process is not
114
+ * running OR no matching button exists, the script reports
115
+ * `not-found`. Any AppleScript exception (e.g. Accessibility
116
+ * permission not granted) is reported as `error:<message>` so the
117
+ * caller can surface it once and continue polling.
118
+ *
119
+ * Requires the Terminal / `unity-mcp-cli` binary to have been granted
120
+ * Accessibility permission in System Settings → Privacy & Security →
121
+ * Accessibility. Documented in the README.
122
+ */
123
+ export declare const MACOS_DISMISS_APPLESCRIPT = "\non run\n try\n tell application \"System Events\"\n if not (exists process \"Unity\") then\n return \"not-found\"\n end if\n tell process \"Unity\"\n repeat with w in windows\n try\n if exists (button \"Ignore\" of w) then\n click button \"Ignore\" of w\n return \"dismissed:Ignore\"\n end if\n end try\n end repeat\n end tell\n end tell\n return \"not-found\"\n on error errMsg\n return \"error:\" & errMsg\n end try\nend run\n";
124
+ /**
125
+ * Reset the cached `xdotool` presence flag. Test-only — production
126
+ * code never needs to call this. Exposed so tests can simulate "the
127
+ * tool was installed mid-process" without affecting other tests.
128
+ */
129
+ export declare function _resetXdotoolPresenceForTests(): void;
130
+ /**
131
+ * Escape a literal string for safe use in `xdotool search --name`.
132
+ * `xdotool search --name` interprets its argument as a regex; without
133
+ * escaping, a future fragment containing metacharacters (e.g.
134
+ * `(Hold On)` or `Compiler Errors v2.0+`) would silently change the
135
+ * match semantics. The current fragments are regex-safe but this
136
+ * helper is defensive and zero-runtime-cost.
137
+ *
138
+ * Exposed for tests so the regex-safety contract is locked down.
139
+ */
140
+ export declare function regexEscapeForXdotool(s: string): string;
141
+ /**
142
+ * Parse the single-token contract every platform-specific dispatcher
143
+ * writes to stdout. Exposed for tests so we can exhaustively cover
144
+ * the parser without invoking PowerShell / osascript / xdotool.
145
+ *
146
+ * Inspects the LAST non-empty line of stdout, not the whole buffer:
147
+ * a stray PowerShell warning, `osascript` deprecation notice, or
148
+ * `xdotool` chatter printed before the contract token must not
149
+ * misclassify the result as `not-found`.
150
+ *
151
+ * Contract:
152
+ * - `dismissed:<button>` → `{ kind: 'dismissed', button }`
153
+ * - `not-found` → `{ kind: 'not-found' }`
154
+ * - `error:<message>` → `{ kind: 'error', message }`
155
+ * - any other / empty → `{ kind: 'not-found' }` (defensive — a
156
+ * transient parse miss is treated as "not yet visible" rather
157
+ * than aborting the polling loop)
158
+ */
159
+ export declare function parseDismissOutput(stdout: string): DismissOutcome;
@@ -0,0 +1,440 @@
1
+ import { execFile, execFileSync } from 'child_process';
2
+ import { platform as nodePlatform } from 'os';
3
+ import { verbose } from './ui.js';
4
+ /**
5
+ * Window-title fragments matched against the Unity launch-errors
6
+ * dialog. Both legacy and current strings are listed so the matcher
7
+ * stays resilient across Unity versions. The match is case-insensitive
8
+ * and substring-based — Unity's actual title varies by version but
9
+ * always contains one of these.
10
+ *
11
+ * Exposed so tests can assert the matcher knows about both spellings
12
+ * without grepping the implementation.
13
+ */
14
+ export const LAUNCH_ERROR_DIALOG_TITLE_FRAGMENTS = [
15
+ 'Safe Mode', // Unity 2020.2+ ("Enter Safe Mode?") — the launch-errors dialog on every modern Unity (2022 LTS, 6000.x)
16
+ 'Compiler Errors', // Unity 2020+ ("Hold On" + "Compiler Errors on Launch")
17
+ 'Hold On', // Generic Unity progress dialog wrapping the launch-errors variant
18
+ 'Compile Errors', // Older Unity dialog spelling
19
+ 'Scripts have compiler errors', // Unity 2022+ on Linux (window manager surfacing)
20
+ ];
21
+ /** The button label this helper presses to dismiss the dialog. */
22
+ export const DISMISS_BUTTON_LABEL = 'Ignore';
23
+ /**
24
+ * Producer-side prefixes for error messages that callers treat as
25
+ * permanent (the polling loop bails out instead of ticking again).
26
+ *
27
+ * Exported so the bailout matcher in `lib/open.ts` and the
28
+ * error-construction sites below reference the SAME literal — a
29
+ * future re-word lands in both places at once.
30
+ *
31
+ * The matcher in `lib/open.ts` uses `String.includes`, so each
32
+ * constant just needs to be a stable substring of the full message.
33
+ */
34
+ export const LINUX_XDOTOOL_MISSING_PREFIX = 'xdotool not found on PATH';
35
+ export const UNSUPPORTED_PLATFORM_PREFIX = 'Unsupported platform for launch-errors auto-dismiss';
36
+ /**
37
+ * Try once to find and dismiss the Unity launch-errors dialog on the
38
+ * current OS desktop. Pure-ish — performs a single OS call and
39
+ * returns; never blocks past the underlying syscall's own timeout.
40
+ *
41
+ * Library-safe: never throws (errors are returned in the
42
+ * `DismissOutcome` union), never writes to stdout/stderr, never
43
+ * mutates global state.
44
+ *
45
+ * The helper is platform-dispatched:
46
+ * - **Windows**: Win32 (`FindWindowW` / `EnumWindows` /
47
+ * `EnumChildWindows` / `GetWindowTextW` / `SendMessageW(BM_CLICK)`)
48
+ * driven from PowerShell so we do not pull in a native node-gyp
49
+ * dependency. UI Automation is the documented fallback if title
50
+ * matching breaks on a future Unity release.
51
+ * - **macOS**: AppleScript via `osascript` (the leaner
52
+ * AX-C-API-direct path is a documented follow-up). Requires the
53
+ * user to have granted Accessibility permission to the terminal /
54
+ * `unity-mcp-cli` binary once.
55
+ * - **Linux/X11**: `xdotool` (documented as a Linux platform
56
+ * dependency; `wmctrl` is acceptable as an alternative window
57
+ * enumerator). Wayland is deferred — call out explicitly in the
58
+ * error message so the user does not waste time debugging.
59
+ */
60
+ export async function tryDismissLaunchErrorsDialog(platform = nodePlatform()) {
61
+ switch (platform) {
62
+ case 'win32':
63
+ return tryDismissWindows();
64
+ case 'darwin':
65
+ return tryDismissMacOS();
66
+ case 'linux':
67
+ return tryDismissLinuxX11();
68
+ default:
69
+ return {
70
+ kind: 'error',
71
+ message: `${UNSUPPORTED_PLATFORM_PREFIX}: ${platform}`,
72
+ };
73
+ }
74
+ }
75
+ // ---------------------------------------------------------------------------
76
+ // Windows — Win32 via PowerShell
77
+ // ---------------------------------------------------------------------------
78
+ /**
79
+ * The PowerShell payload that probes for the Unity launch-errors
80
+ * dialog and clicks `Ignore` if found. Exported as a string (not a
81
+ * function) so tests can assert the script shape without launching
82
+ * PowerShell.
83
+ *
84
+ * Strategy:
85
+ * 1. P/Invoke `EnumWindows` via Add-Type to enumerate every visible
86
+ * top-level window owned by `Unity.exe`.
87
+ * 2. Filter by title fragment (case-insensitive substring).
88
+ * 3. Walk child windows with `EnumChildWindows`, looking for a
89
+ * Button whose text equals `Ignore`.
90
+ * 4. Send `BM_CLICK` (0x00F5) to the matched button. `BM_CLICK` is
91
+ * preferred over a synthesised mouse event — it works even if
92
+ * the user is mid-click in another app, and it does not steal
93
+ * focus.
94
+ *
95
+ * The script writes a single-token result to stdout:
96
+ * - `dismissed:<button>` on success
97
+ * - `not-found` when no dialog was matched
98
+ * - `error:<message>` on an unexpected exception
99
+ */
100
+ export const WINDOWS_DISMISS_PS_SCRIPT = `
101
+ $ErrorActionPreference = 'Stop'
102
+ try {
103
+ if (-not ([System.Management.Automation.PSTypeName]'UnityMcp.LaunchErrors.Dismisser').Type) {
104
+ Add-Type -TypeDefinition @"
105
+ using System;
106
+ using System.Collections.Generic;
107
+ using System.Runtime.InteropServices;
108
+ using System.Text;
109
+ namespace UnityMcp.LaunchErrors {
110
+ public static class Dismisser {
111
+ public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
112
+ [DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
113
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetWindowTextW(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
114
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern int GetClassNameW(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
115
+ [DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr hWnd);
116
+ [DllImport("user32.dll")] static extern bool EnumChildWindows(IntPtr hWndParent, EnumWindowsProc lpEnumFunc, IntPtr lParam);
117
+ [DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
118
+ [DllImport("user32.dll", CharSet=CharSet.Unicode)] static extern IntPtr SendMessageW(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
119
+ const uint BM_CLICK = 0x00F5;
120
+ static readonly string[] TitleFragments = new[] { ${LAUNCH_ERROR_DIALOG_TITLE_FRAGMENTS.map((f) => JSON.stringify(f)).join(', ')} };
121
+ static readonly string[] ButtonLabels = new[] { "${DISMISS_BUTTON_LABEL}", "&${DISMISS_BUTTON_LABEL}" };
122
+ public static string TryDismiss(int[] unityPids) {
123
+ var unityPidSet = new HashSet<uint>();
124
+ for (int i = 0; i < unityPids.Length; i++) unityPidSet.Add((uint)unityPids[i]);
125
+ IntPtr matchedDialog = IntPtr.Zero;
126
+ var sb = new StringBuilder(512);
127
+ EnumWindows((hWnd, lParam) => {
128
+ if (!IsWindowVisible(hWnd)) return true;
129
+ uint procId; GetWindowThreadProcessId(hWnd, out procId);
130
+ if (!unityPidSet.Contains(procId)) return true;
131
+ sb.Length = 0;
132
+ GetWindowTextW(hWnd, sb, sb.Capacity);
133
+ var text = sb.ToString();
134
+ foreach (var frag in TitleFragments) {
135
+ if (text.IndexOf(frag, StringComparison.OrdinalIgnoreCase) >= 0) {
136
+ matchedDialog = hWnd;
137
+ return false;
138
+ }
139
+ }
140
+ return true;
141
+ }, IntPtr.Zero);
142
+ if (matchedDialog == IntPtr.Zero) return "not-found";
143
+ IntPtr matchedButton = IntPtr.Zero;
144
+ EnumChildWindows(matchedDialog, (hWnd, lParam) => {
145
+ sb.Length = 0;
146
+ GetClassNameW(hWnd, sb, sb.Capacity);
147
+ if (sb.ToString() != "Button") return true;
148
+ sb.Length = 0;
149
+ GetWindowTextW(hWnd, sb, sb.Capacity);
150
+ var text = sb.ToString();
151
+ foreach (var label in ButtonLabels) {
152
+ if (string.Equals(text, label, StringComparison.OrdinalIgnoreCase)) {
153
+ matchedButton = hWnd;
154
+ return false;
155
+ }
156
+ }
157
+ return true;
158
+ }, IntPtr.Zero);
159
+ if (matchedButton == IntPtr.Zero) return "not-found";
160
+ SendMessageW(matchedButton, BM_CLICK, IntPtr.Zero, IntPtr.Zero);
161
+ return "dismissed:${DISMISS_BUTTON_LABEL}";
162
+ }
163
+ }
164
+ }
165
+ "@
166
+ }
167
+ $unityPids = @(Get-Process -Name 'Unity' -ErrorAction SilentlyContinue | ForEach-Object { [int]$_.Id })
168
+ if ($unityPids.Count -eq 0) { Write-Output 'not-found'; return }
169
+ Write-Output ([UnityMcp.LaunchErrors.Dismisser]::TryDismiss([int[]]$unityPids))
170
+ } catch {
171
+ Write-Output ('error:' + $_.Exception.Message)
172
+ }
173
+ `;
174
+ async function tryDismissWindows() {
175
+ return new Promise((resolve) => {
176
+ execFile('powershell', ['-NoProfile', '-NonInteractive', '-Command', WINDOWS_DISMISS_PS_SCRIPT], { timeout: 5000, windowsHide: true }, (err, stdout) => {
177
+ if (err) {
178
+ // ETIMEDOUT, ENOENT (powershell missing), etc. Treat as transient.
179
+ verbose(`tryDismissWindows: powershell invocation failed (${err.message}) — treating as not-found`);
180
+ resolve({ kind: 'error', message: err.message });
181
+ return;
182
+ }
183
+ resolve(parseDismissOutput(stdout));
184
+ });
185
+ });
186
+ }
187
+ // ---------------------------------------------------------------------------
188
+ // macOS — AppleScript via osascript
189
+ // ---------------------------------------------------------------------------
190
+ /**
191
+ * The AppleScript snippet used for the macOS dismiss path. Exposed as
192
+ * a string for testability.
193
+ *
194
+ * Strategy: iterate every window of the Unity application process and
195
+ * click the first button titled `Ignore`. If the Unity process is not
196
+ * running OR no matching button exists, the script reports
197
+ * `not-found`. Any AppleScript exception (e.g. Accessibility
198
+ * permission not granted) is reported as `error:<message>` so the
199
+ * caller can surface it once and continue polling.
200
+ *
201
+ * Requires the Terminal / `unity-mcp-cli` binary to have been granted
202
+ * Accessibility permission in System Settings → Privacy & Security →
203
+ * Accessibility. Documented in the README.
204
+ */
205
+ export const MACOS_DISMISS_APPLESCRIPT = `
206
+ on run
207
+ try
208
+ tell application "System Events"
209
+ if not (exists process "Unity") then
210
+ return "not-found"
211
+ end if
212
+ tell process "Unity"
213
+ repeat with w in windows
214
+ try
215
+ if exists (button "${DISMISS_BUTTON_LABEL}" of w) then
216
+ click button "${DISMISS_BUTTON_LABEL}" of w
217
+ return "dismissed:${DISMISS_BUTTON_LABEL}"
218
+ end if
219
+ end try
220
+ end repeat
221
+ end tell
222
+ end tell
223
+ return "not-found"
224
+ on error errMsg
225
+ return "error:" & errMsg
226
+ end try
227
+ end run
228
+ `;
229
+ async function tryDismissMacOS() {
230
+ return new Promise((resolve) => {
231
+ execFile('osascript', ['-e', MACOS_DISMISS_APPLESCRIPT], { timeout: 5000 }, (err, stdout) => {
232
+ if (err) {
233
+ verbose(`tryDismissMacOS: osascript invocation failed (${err.message}) — treating as transient error`);
234
+ resolve({ kind: 'error', message: err.message });
235
+ return;
236
+ }
237
+ resolve(parseDismissOutput(stdout));
238
+ });
239
+ });
240
+ }
241
+ // ---------------------------------------------------------------------------
242
+ // Linux/X11 — xdotool
243
+ // ---------------------------------------------------------------------------
244
+ /**
245
+ * Whether `xdotool` is on PATH. Cached per process so the polling
246
+ * loop pays the lookup cost at most once. `which xdotool` /
247
+ * `command -v xdotool` is too coarse here — we directly try
248
+ * `xdotool --version` and observe the exit code.
249
+ */
250
+ let xdotoolPresence;
251
+ function isXdotoolAvailable() {
252
+ if (xdotoolPresence !== undefined)
253
+ return xdotoolPresence;
254
+ try {
255
+ execFileSync('xdotool', ['--version'], {
256
+ stdio: 'ignore',
257
+ timeout: 2000,
258
+ });
259
+ xdotoolPresence = true;
260
+ }
261
+ catch {
262
+ xdotoolPresence = false;
263
+ }
264
+ return xdotoolPresence;
265
+ }
266
+ /**
267
+ * Reset the cached `xdotool` presence flag. Test-only — production
268
+ * code never needs to call this. Exposed so tests can simulate "the
269
+ * tool was installed mid-process" without affecting other tests.
270
+ */
271
+ export function _resetXdotoolPresenceForTests() {
272
+ xdotoolPresence = undefined;
273
+ }
274
+ /**
275
+ * Escape a literal string for safe use in `xdotool search --name`.
276
+ * `xdotool search --name` interprets its argument as a regex; without
277
+ * escaping, a future fragment containing metacharacters (e.g.
278
+ * `(Hold On)` or `Compiler Errors v2.0+`) would silently change the
279
+ * match semantics. The current fragments are regex-safe but this
280
+ * helper is defensive and zero-runtime-cost.
281
+ *
282
+ * Exposed for tests so the regex-safety contract is locked down.
283
+ */
284
+ export function regexEscapeForXdotool(s) {
285
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
286
+ }
287
+ /**
288
+ * Look up currently-running Unity Editor PIDs on the local box.
289
+ * Used by the Linux/X11 dismiss path to scope the title-fragment
290
+ * window match to Unity processes only — without this, `xdotool
291
+ * search --name 'Hold On'` would match any top-level window whose
292
+ * title contains "Hold On" (browser tab, chat client, etc.).
293
+ *
294
+ * Returns an empty array on any failure (missing `pgrep`, no Unity
295
+ * running, syscall error). Pure / idempotent / never throws.
296
+ */
297
+ function getUnityPidsLinux() {
298
+ try {
299
+ const stdout = execFileSync('pgrep', ['-x', 'Unity'], {
300
+ stdio: ['ignore', 'pipe', 'ignore'],
301
+ timeout: 1000,
302
+ encoding: 'utf8',
303
+ });
304
+ return stdout
305
+ .split(/\r?\n/)
306
+ .map((s) => parseInt(s.trim(), 10))
307
+ .filter((n) => Number.isFinite(n) && n > 0);
308
+ }
309
+ catch {
310
+ return [];
311
+ }
312
+ }
313
+ async function tryDismissLinuxX11() {
314
+ if (!isXdotoolAvailable()) {
315
+ return {
316
+ kind: 'error',
317
+ message: `${LINUX_XDOTOOL_MISSING_PREFIX}. Install it (e.g. \`sudo apt-get install xdotool\`) to enable Unity launch-errors auto-dismiss on Linux/X11. Wayland is not yet supported.`,
318
+ };
319
+ }
320
+ const unityPids = new Set(getUnityPidsLinux());
321
+ if (unityPids.size === 0) {
322
+ return { kind: 'not-found' };
323
+ }
324
+ // Strategy:
325
+ // 1. `xdotool search --name '<regex-escaped fragment>'` once per
326
+ // fragment, collect candidate window IDs. Empty stdout from
327
+ // `xdotool search` is reported via a non-zero exit; treat that
328
+ // as "no match".
329
+ // 2. Filter candidate windows to those owned by a Unity PID via
330
+ // `xdotool getwindowpid <winId>` so we never act on an
331
+ // unrelated top-level window with a colliding title.
332
+ // 3. Activate the window (`windowactivate --sync`) and send Return
333
+ // to click the focused button. The Unity launch-errors dialog
334
+ // defaults focus to the leftmost button, which IS `Ignore`.
335
+ return new Promise((resolve) => {
336
+ const fragments = LAUNCH_ERROR_DIALOG_TITLE_FRAGMENTS;
337
+ let idx = 0;
338
+ const tryNext = () => {
339
+ if (idx >= fragments.length) {
340
+ resolve({ kind: 'not-found' });
341
+ return;
342
+ }
343
+ const fragment = fragments[idx++];
344
+ execFile('xdotool', ['search', '--name', regexEscapeForXdotool(fragment)], { timeout: 2000 }, (err, stdout) => {
345
+ if (err || !stdout.trim()) {
346
+ tryNext();
347
+ return;
348
+ }
349
+ const candidateIds = stdout.trim().split(/\s+/).filter(Boolean);
350
+ findUnityOwnedWindow(candidateIds, unityPids, (winId) => {
351
+ if (!winId) {
352
+ tryNext();
353
+ return;
354
+ }
355
+ // Activate then send Return to click the focused (Ignore) button.
356
+ execFile('xdotool', ['windowactivate', '--sync', winId, 'key', '--clearmodifiers', 'Return'], { timeout: 2000 }, (activateErr) => {
357
+ if (activateErr) {
358
+ resolve({
359
+ kind: 'error',
360
+ message: `xdotool failed to dismiss window ${winId}: ${activateErr.message}`,
361
+ });
362
+ return;
363
+ }
364
+ resolve({ kind: 'dismissed', button: DISMISS_BUTTON_LABEL });
365
+ });
366
+ });
367
+ });
368
+ };
369
+ tryNext();
370
+ });
371
+ }
372
+ /**
373
+ * Walk `candidateIds` and call `done(winId)` with the first window
374
+ * owned by a Unity PID, or `done(undefined)` if none match. Each
375
+ * `xdotool getwindowpid <id>` call has a short timeout so a stalled
376
+ * lookup cannot stall the polling loop. Sequential, not parallel —
377
+ * the candidate list is small and parallelism would buy us nothing
378
+ * meaningful here.
379
+ */
380
+ function findUnityOwnedWindow(candidateIds, unityPids, done) {
381
+ let idx = 0;
382
+ const next = () => {
383
+ if (idx >= candidateIds.length) {
384
+ done(undefined);
385
+ return;
386
+ }
387
+ const winId = candidateIds[idx++];
388
+ execFile('xdotool', ['getwindowpid', winId], { timeout: 1000 }, (err, stdout) => {
389
+ if (err) {
390
+ next();
391
+ return;
392
+ }
393
+ const pid = parseInt(stdout.trim(), 10);
394
+ if (Number.isFinite(pid) && unityPids.has(pid)) {
395
+ done(winId);
396
+ return;
397
+ }
398
+ next();
399
+ });
400
+ };
401
+ next();
402
+ }
403
+ // ---------------------------------------------------------------------------
404
+ // Shared helpers
405
+ // ---------------------------------------------------------------------------
406
+ /**
407
+ * Parse the single-token contract every platform-specific dispatcher
408
+ * writes to stdout. Exposed for tests so we can exhaustively cover
409
+ * the parser without invoking PowerShell / osascript / xdotool.
410
+ *
411
+ * Inspects the LAST non-empty line of stdout, not the whole buffer:
412
+ * a stray PowerShell warning, `osascript` deprecation notice, or
413
+ * `xdotool` chatter printed before the contract token must not
414
+ * misclassify the result as `not-found`.
415
+ *
416
+ * Contract:
417
+ * - `dismissed:<button>` → `{ kind: 'dismissed', button }`
418
+ * - `not-found` → `{ kind: 'not-found' }`
419
+ * - `error:<message>` → `{ kind: 'error', message }`
420
+ * - any other / empty → `{ kind: 'not-found' }` (defensive — a
421
+ * transient parse miss is treated as "not yet visible" rather
422
+ * than aborting the polling loop)
423
+ */
424
+ export function parseDismissOutput(stdout) {
425
+ const lines = stdout.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0);
426
+ if (lines.length === 0)
427
+ return { kind: 'not-found' };
428
+ const last = lines[lines.length - 1];
429
+ if (last === 'not-found')
430
+ return { kind: 'not-found' };
431
+ if (last.startsWith('dismissed:')) {
432
+ const button = last.substring('dismissed:'.length);
433
+ return { kind: 'dismissed', button: button || DISMISS_BUTTON_LABEL };
434
+ }
435
+ if (last.startsWith('error:')) {
436
+ return { kind: 'error', message: last.substring('error:'.length) };
437
+ }
438
+ return { kind: 'not-found' };
439
+ }
440
+ //# sourceMappingURL=launch-error-dismiss.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"launch-error-dismiss.js","sourceRoot":"","sources":["../../src/utils/launch-error-dismiss.ts"],"names":[],"mappings":"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"}