Skip to content

Commit 6083e12

Browse files
authored
unity-mcp-cli: auto-dismiss Unity Editor "compile errors at launch" dialog so the resolver can run (Win/macOS/Linux) (#738)
feat(cli): auto-dismiss Unity Editor launch-errors dialog. Pass-1 simplify: abort-signal + grace-window early-exit, recurrence-aware loop, Unity-PID scoping on Linux, regex-escape, parser hardened, permanent-error bailout, interval 500->1500ms. Pass-2 simplify: drop in-flight probe outcomes when abort fires mid-await, gate timeout warning on deadline-not-abort, extract producer-side prefix constants. Pass-3 review returned [none]. 346/347 CLI tests pass. Closes #737
1 parent 6ed5eb7 commit 6083e12

8 files changed

Lines changed: 1559 additions & 5 deletions

File tree

cli/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,21 @@ cd ./MyGame && unity-mcp-cli open
255255
| `--tools <names>` | `UNITY_MCP_TOOLS` | No | Comma-separated list of tools to enable |
256256
| `--transport <method>` | `UNITY_MCP_TRANSPORT` | No | Transport method: `streamableHttp` or `stdio` |
257257
| `--start-server <value>` | `UNITY_MCP_START_SERVER` | No | Set to `true` or `false` to control MCP server auto-start |
258+
| `--no-auto-dismiss-launch-errors` || No | Disable auto-dismissal of the Unity Editor "compile errors at launch" dialog (default: enabled) |
259+
| `--launch-dismiss-timeout-ms <ms>` || No | Overall timeout (milliseconds) for the launch-errors auto-dismiss polling loop (default: `30000`) |
260+
| `--launch-dismiss-poll-interval-ms <ms>` || No | Polling tick interval (milliseconds) for the launch-errors auto-dismiss loop (default: `1500`) |
258261

259-
The editor process is spawned in detached mode — the CLI returns immediately.
262+
The editor process is spawned in detached mode. By default, after spawning the editor, `open` polls for Unity's "compile errors at launch" dialog and clicks `Ignore` so the editor finishes initialising — without this, any in-Editor automation that needs to run after a state where Unity itself can't compile (e.g. the NuGet dependency resolver) cannot self-heal. The dialog appears within seconds of editor launch when it appears at all, so when no dialog has been seen within a short grace window after polling starts the loop exits early — the no-dialog case adds at most that grace window's delay, never the full `--launch-dismiss-timeout-ms`. If the dialog is observed (and successfully dismissed), polling continues until the overall timeout so a re-appearing dialog (resolver fixes one error → dialog re-surfaces with the next) is dismissed again. Library-mode callers can supply an `AbortSignal` (`launchDismissAbortSignal` on `OpenProjectOptions`) to abort the loop the instant their own readiness signal fires.
263+
264+
### Auto-dismiss platform requirements
265+
266+
| Platform | Requirement | Notes |
267+
|---|---|---|
268+
| **Windows** | Built-in (Win32 API) | Uses `EnumWindows` / `EnumChildWindows` / `SendMessageW(BM_CLICK)` driven from PowerShell. No extra setup required. |
269+
| **macOS** | **Accessibility permission** must be granted to the terminal (or `unity-mcp-cli` binary). System Settings → Privacy & Security → Accessibility. | Implemented via AppleScript / `osascript`. Without this permission, `osascript` reports an error every poll tick and the dialog cannot be dismissed. |
270+
| **Linux/X11** | `xdotool` on `PATH` (e.g. `sudo apt-get install xdotool`). | Wayland is **not** supported in the first cut — track upstream issues for Wayland support. |
271+
272+
To opt out entirely, pass `--no-auto-dismiss-launch-errors`.
260273

261274
**Example — open with MCP connection:**
262275

cli/src/commands/open.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,41 @@ export function resolveOpenProjectPath(
4040
/** Re-export for backward compatibility with existing tests. */
4141
export const isUnityProjectDir = libIsUnityProjectDir;
4242

43+
/**
44+
* Parse a `--xxx <ms>` style commander option as a positive integer.
45+
* Commander stores the raw string (including the literal default
46+
* supplied to `.option()`), so a parse-and-validate step is required
47+
* before the value can flow into the library. Exits with code 1 on
48+
* a non-numeric / non-positive value — matches the CLI's existing
49+
* error-handling pattern for malformed flag values (`--auth`,
50+
* `--transport`, `--start-server`).
51+
*
52+
* Exported for unit tests; the CLI wrapper is the only production
53+
* caller.
54+
*/
55+
export function parsePositiveIntFlag(
56+
raw: string | undefined,
57+
flagName: string,
58+
fallback: number,
59+
): number {
60+
if (raw === undefined) return fallback;
61+
const trimmed = String(raw).trim();
62+
if (trimmed === '') return fallback;
63+
// Reject scientific notation, fractions, and explicit signs — the
64+
// ms value MUST be a positive whole number for the polling loop's
65+
// bookkeeping to be sane.
66+
if (!/^\d+$/.test(trimmed)) {
67+
ui.error(`${flagName} must be a positive integer (got: "${raw}")`);
68+
process.exit(1);
69+
}
70+
const parsed = parseInt(trimmed, 10);
71+
if (!Number.isFinite(parsed) || parsed < 0) {
72+
ui.error(`${flagName} must be a positive integer (got: "${raw}")`);
73+
process.exit(1);
74+
}
75+
return parsed;
76+
}
77+
4378
/**
4479
* Print actionable help when a required Unity Editor version is not found.
4580
* Lists installed editors and suggests install or override commands. Lives
@@ -92,6 +127,20 @@ export const openCommand = new Command('open')
92127
.option('--keep-connected', 'Force keep connected (sets UNITY_MCP_KEEP_CONNECTED=true)')
93128
.option('--transport <method>', 'Transport method: streamableHttp or stdio (sets UNITY_MCP_TRANSPORT)')
94129
.option('--start-server <value>', 'Set to true/false to control server auto-start (sets UNITY_MCP_START_SERVER)', undefined)
130+
.option(
131+
'--no-auto-dismiss-launch-errors',
132+
'Disable auto-dismissal of the Unity Editor "compile errors at launch" dialog (default: enabled). On macOS, requires Accessibility permission for the terminal / unity-mcp-cli binary. On Linux/X11, requires `xdotool` on PATH (Wayland not supported).',
133+
)
134+
.option(
135+
'--launch-dismiss-timeout-ms <ms>',
136+
'Overall timeout (milliseconds) for the launch-errors auto-dismiss polling loop (default: 30000)',
137+
'30000',
138+
)
139+
.option(
140+
'--launch-dismiss-poll-interval-ms <ms>',
141+
'Polling tick interval (milliseconds) for the launch-errors auto-dismiss loop (default: 1500)',
142+
'1500',
143+
)
95144
.action(async (positionalPath: string | undefined, options: {
96145
path?: string;
97146
unity?: string;
@@ -103,6 +152,9 @@ export const openCommand = new Command('open')
103152
keepConnected?: boolean;
104153
transport?: string;
105154
startServer?: string;
155+
autoDismissLaunchErrors?: boolean;
156+
launchDismissTimeoutMs?: string;
157+
launchDismissPollIntervalMs?: string;
106158
}) => {
107159
// Resolve the path the same way the library does, but also
108160
// validate the existence + Unity-project shape up front so we
@@ -159,6 +211,25 @@ export const openCommand = new Command('open')
159211
startServerBool = val === 'true';
160212
}
161213

214+
// --launch-dismiss-timeout-ms / --launch-dismiss-poll-interval-ms
215+
// are commander string options with numeric defaults. Parse and
216+
// validate here so the failure mode is a clear CLI error rather
217+
// than a silent NaN that disables the polling loop.
218+
const launchDismissTimeoutMs = parsePositiveIntFlag(
219+
options.launchDismissTimeoutMs,
220+
'--launch-dismiss-timeout-ms',
221+
30000,
222+
);
223+
const launchDismissPollIntervalMs = parsePositiveIntFlag(
224+
options.launchDismissPollIntervalMs,
225+
'--launch-dismiss-poll-interval-ms',
226+
1500,
227+
);
228+
// Commander's `--no-auto-dismiss-launch-errors` produces
229+
// `autoDismissLaunchErrors: false`; the absence of the flag
230+
// produces `undefined` (which `openProject` treats as the default
231+
// `true`). No additional parsing needed.
232+
162233
// Pre-flight already-running check so we don't flash the
163234
// "Locating Unity Editor..." spinner when Unity is already open
164235
// for this project. The lib does its own check too (single
@@ -183,6 +254,9 @@ export const openCommand = new Command('open')
183254
keepConnected: options.keepConnected,
184255
transport: options.transport as OpenProjectTransport | undefined,
185256
startServer: startServerBool,
257+
autoDismissLaunchErrors: options.autoDismissLaunchErrors !== false,
258+
launchDismissTimeoutMs,
259+
launchDismissPollIntervalMs,
186260
onProgress: (event) => {
187261
switch (event.phase) {
188262
case 'detecting-editor-version': {
@@ -234,6 +308,15 @@ export const openCommand = new Command('open')
234308
ui.success(`Launched Unity Editor (PID: ${event.pid ?? 'unknown'})`);
235309
break;
236310
}
311+
case 'launch-errors-dismissed': {
312+
// Single info-level log line on dismissal — exact format
313+
// is part of the issue's acceptance contract. Keep in
314+
// sync with `launch-error-dismiss.ts` if changed.
315+
ui.info(
316+
`[open] dismissed Unity launch-errors dialog (button=${event.button}, platform=${event.platform})`,
317+
);
318+
break;
319+
}
237320
default:
238321
break;
239322
}

0 commit comments

Comments
 (0)