unity-mcp-cli 0.66.1 → 0.67.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/README.md CHANGED
@@ -96,6 +96,7 @@ npx unity-mcp-cli install-plugin /path/to/unity/project
96
96
  - [`install-plugin`](#install-plugin)
97
97
  - [`install-unity`](#install-unity)
98
98
  - [`open`](#open)
99
+ - [`close`](#close)
99
100
  - [`run-tool`](#run-tool)
100
101
  - [`wait-for-ready`](#wait-for-ready)
101
102
  - [`setup-mcp`](#setup-mcp)
@@ -283,6 +284,48 @@ unity-mcp-cli open ./MyGame \
283
284
 
284
285
  ![AI Game Developer — Unity SKILLS and MCP](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/promo/hazzard-divider.svg?raw=true)
285
286
 
287
+ ## `close`
288
+
289
+ Gracefully terminate the Unity Editor instance running for a given project path. Symmetric counterpart of [`open`](#open) — for scripted workflows (CI agents, pipeline executors, integration test fixtures) that need a clean tear-down without resorting to OS-level process kills.
290
+
291
+ ```bash
292
+ unity-mcp-cli close ./MyGame
293
+ ```
294
+
295
+ | Option | Required | Description |
296
+ |---|---|---|
297
+ | `[path]` | No | Path to the Unity project (positional, defaults to current directory) |
298
+ | `--timeout <seconds>` | No | Polite-quit timeout in seconds (default: 30) |
299
+ | `--force` | No | Hard-kill the Editor if it does not exit within `--timeout` |
300
+
301
+ **How it works:**
302
+
303
+ 1. Resolves the running Editor's PID by reading `<project>/Temp/UnityLockfile` (4-byte little-endian uint32) and cross-checking against process enumeration to handle stale lock files.
304
+ 2. Sends a polite-quit signal — `SIGTERM` on Linux/macOS, `taskkill` (no `/F`) on Windows — letting Unity finish autosave / asset-import.
305
+ 3. Polls every 250ms until the process exits or `--timeout` elapses.
306
+ 4. If the timeout expires AND `--force` is set, falls back to `SIGKILL` / `taskkill /F`.
307
+ 5. Idempotent — closing an already-closed Editor (or a project whose Editor was never running) exits 0 with `no running Editor for project at <path>`.
308
+ 6. Refuses to act on any path that is not a Unity project root (`ProjectSettings/ProjectVersion.txt` must exist) — protects against accidental kill-all-Unity-on-host invocations.
309
+
310
+ > **Windows headless caveat:** the polite-quit step uses `taskkill` (no `/F`), which delivers `WM_CLOSE`. That message only reaches processes owning a top-level window on the **same desktop/session** as the CLI. If Unity was launched by a Windows service in session 0 (or any other non-interactive desktop), the polite-quit will be silently dropped, the `--timeout` will elapse, and `--force` becomes the only path that brings the Editor down. Plan accordingly in headless CI runners.
311
+
312
+ **Example — close, fall back to force after 60s:**
313
+
314
+ ```bash
315
+ unity-mcp-cli close ./MyGame --timeout 60 --force
316
+ ```
317
+
318
+ **Example — clean tear-down at the end of an automation script:**
319
+
320
+ ```bash
321
+ unity-mcp-cli open ./MyGame
322
+ unity-mcp-cli wait-for-ready ./MyGame
323
+ unity-mcp-cli run-tool tests-run ./MyGame --input '{"testMode":"EditMode"}'
324
+ unity-mcp-cli close ./MyGame
325
+ ```
326
+
327
+ ![AI Game Developer — Unity SKILLS and MCP](https://github.com/IvanMurzak/Unity-MCP/blob/main/docs/img/promo/hazzard-divider.svg?raw=true)
328
+
286
329
  ## `run-tool`
287
330
 
288
331
  Execute an MCP tool directly via the HTTP API. The server URL and authorization token are **automatically resolved** from the project's config file (`UserSettings/AI-Game-Developer-Config.json`), based on the current connection mode (Custom or Cloud).
@@ -0,0 +1,56 @@
1
+ import { Command } from 'commander';
2
+ import { type SupportedPlatform } from '../utils/unity-shutdown.js';
3
+ export interface CloseOptions {
4
+ timeout?: string;
5
+ force?: boolean;
6
+ }
7
+ /**
8
+ * Resolve the project path argument to an absolute, canonical path.
9
+ *
10
+ * Mirrors the convention used by `open` / `wait-for-ready` — accepts a
11
+ * positional argument or `process.cwd()` fallback, normalises symlinks where
12
+ * possible, and trims any trailing path separator. Exported for unit tests.
13
+ */
14
+ export declare function resolveCloseProjectPath(positionalPath: string | undefined, cwd: string): string;
15
+ /**
16
+ * Returns true when `<projectPath>/ProjectSettings/ProjectVersion.txt` exists.
17
+ *
18
+ * The issue's acceptance criteria require refusing to act on any path that
19
+ * does not look like a Unity project root (`ProjectVersion.txt` is the
20
+ * canonical marker). This is intentionally narrower than the `open`
21
+ * subcommand's check — the goal here is to defend against accidental
22
+ * "kill all Unity processes on host" invocations, so a positive ID of
23
+ * "this is a Unity project" is required.
24
+ *
25
+ * Exported for unit tests.
26
+ */
27
+ export declare function isUnityProjectRoot(projectPath: string): boolean;
28
+ /**
29
+ * Parse a positive-integer `--timeout` value (seconds). Returns the parsed
30
+ * value, or `null` when the input is not a valid positive integer.
31
+ *
32
+ * The `undefined` branch falls back to the default for ergonomic
33
+ * direct-call use (e.g., unit tests). The action handler never reaches it
34
+ * because commander always materialises the configured default string.
35
+ *
36
+ * Exported for unit tests.
37
+ */
38
+ export declare function parseTimeoutSeconds(raw: string | undefined): number | null;
39
+ /**
40
+ * Resolve the editor PID for a given project path.
41
+ *
42
+ * Reads `<project>/Temp/UnityLockfile` (4 bytes LE uint32) and enumerates
43
+ * Unity processes by command line, then prefers the lockfile PID when it
44
+ * is alive and matches the enumerated process for this project (handles
45
+ * stale lockfiles); otherwise uses the enumerated PID.
46
+ *
47
+ * Symlink/realpath handling: `resolveCloseProjectPath` canonicalises via
48
+ * `realpathSync` while `findUnityProcess` only `path.resolve`s the cmdline
49
+ * `-projectPath` argument. If Unity was launched via a symlink, the first
50
+ * lookup misses. We retry against the un-canonicalised form so a symlinked
51
+ * launch path doesn't silently degrade close to a no-op.
52
+ *
53
+ * Returns `null` when no live editor matches the project. Exported for tests.
54
+ */
55
+ export declare function resolveEditorPid(projectPath: string, platform?: SupportedPlatform): number | null;
56
+ export declare const closeCommand: Command;
@@ -0,0 +1,186 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { platform as nodePlatform } from 'os';
5
+ import * as ui from '../utils/ui.js';
6
+ import { verbose } from '../utils/ui.js';
7
+ import { findUnityProcess } from '../utils/unity-process.js';
8
+ import { readLockfilePid, isProcessAlive, sendGracefulShutdown, sendForceKill, waitForExit, } from '../utils/unity-shutdown.js';
9
+ const DEFAULT_TIMEOUT_SECONDS = 30;
10
+ /**
11
+ * Resolve the project path argument to an absolute, canonical path.
12
+ *
13
+ * Mirrors the convention used by `open` / `wait-for-ready` — accepts a
14
+ * positional argument or `process.cwd()` fallback, normalises symlinks where
15
+ * possible, and trims any trailing path separator. Exported for unit tests.
16
+ */
17
+ export function resolveCloseProjectPath(positionalPath, cwd) {
18
+ const explicit = positionalPath ?? cwd;
19
+ let resolved = path.resolve(explicit);
20
+ // realpathSync collapses symlinks, ".." segments, and trailing separators
21
+ // when the path exists. When it does not, fall through to the resolved
22
+ // form so the caller can produce a "path does not exist" error.
23
+ try {
24
+ resolved = fs.realpathSync(resolved);
25
+ }
26
+ catch {
27
+ // Path may not exist — let the caller decide what to do.
28
+ }
29
+ return resolved;
30
+ }
31
+ /**
32
+ * Returns true when `<projectPath>/ProjectSettings/ProjectVersion.txt` exists.
33
+ *
34
+ * The issue's acceptance criteria require refusing to act on any path that
35
+ * does not look like a Unity project root (`ProjectVersion.txt` is the
36
+ * canonical marker). This is intentionally narrower than the `open`
37
+ * subcommand's check — the goal here is to defend against accidental
38
+ * "kill all Unity processes on host" invocations, so a positive ID of
39
+ * "this is a Unity project" is required.
40
+ *
41
+ * Exported for unit tests.
42
+ */
43
+ export function isUnityProjectRoot(projectPath) {
44
+ return fs.existsSync(path.join(projectPath, 'ProjectSettings', 'ProjectVersion.txt'));
45
+ }
46
+ /**
47
+ * Parse a positive-integer `--timeout` value (seconds). Returns the parsed
48
+ * value, or `null` when the input is not a valid positive integer.
49
+ *
50
+ * The `undefined` branch falls back to the default for ergonomic
51
+ * direct-call use (e.g., unit tests). The action handler never reaches it
52
+ * because commander always materialises the configured default string.
53
+ *
54
+ * Exported for unit tests.
55
+ */
56
+ export function parseTimeoutSeconds(raw) {
57
+ if (raw === undefined)
58
+ return DEFAULT_TIMEOUT_SECONDS;
59
+ const parsed = Number(raw);
60
+ if (!Number.isInteger(parsed) || parsed <= 0)
61
+ return null;
62
+ return parsed;
63
+ }
64
+ /**
65
+ * Resolve the editor PID for a given project path.
66
+ *
67
+ * Reads `<project>/Temp/UnityLockfile` (4 bytes LE uint32) and enumerates
68
+ * Unity processes by command line, then prefers the lockfile PID when it
69
+ * is alive and matches the enumerated process for this project (handles
70
+ * stale lockfiles); otherwise uses the enumerated PID.
71
+ *
72
+ * Symlink/realpath handling: `resolveCloseProjectPath` canonicalises via
73
+ * `realpathSync` while `findUnityProcess` only `path.resolve`s the cmdline
74
+ * `-projectPath` argument. If Unity was launched via a symlink, the first
75
+ * lookup misses. We retry against the un-canonicalised form so a symlinked
76
+ * launch path doesn't silently degrade close to a no-op.
77
+ *
78
+ * Returns `null` when no live editor matches the project. Exported for tests.
79
+ */
80
+ export function resolveEditorPid(projectPath, platform = nodePlatform()) {
81
+ const lockPid = readLockfilePid(projectPath);
82
+ // Enumerate Unity processes once. `findUnityProcess` shells out (3s timeout
83
+ // on Windows via PowerShell+CIM, 5s on POSIX via `ps`), so calling it twice
84
+ // on the stale-lockfile path used to roughly double close latency.
85
+ let proc = findUnityProcess(projectPath);
86
+ // Symlink fallback: if the canonical realpath form found no process, retry
87
+ // with `path.resolve` only (no realpath collapse) — that matches what
88
+ // `findUnityProcess` derives from the cmdline `-projectPath` argument.
89
+ if (!proc) {
90
+ const resolvedNoRealpath = path.resolve(projectPath);
91
+ if (resolvedNoRealpath !== projectPath) {
92
+ proc = findUnityProcess(resolvedNoRealpath);
93
+ }
94
+ }
95
+ if (lockPid !== null && isProcessAlive(lockPid, platform)) {
96
+ if (proc && proc.pid === lockPid) {
97
+ verbose(`Lockfile PID ${lockPid} confirmed via process enumeration`);
98
+ return lockPid;
99
+ }
100
+ // Lockfile alive but no matching enumerated process — the lockfile may
101
+ // still be authoritative if the asymmetry is purely realpath/symlink
102
+ // related (process listed under symlink path while we queried by realpath).
103
+ // Trust the live lockfile PID in that case rather than discarding it.
104
+ if (!proc) {
105
+ verbose(`Lockfile PID ${lockPid} alive but no enumerated match — using lockfile PID (likely symlink/path mismatch)`);
106
+ return lockPid;
107
+ }
108
+ verbose(`Lockfile PID ${lockPid} did not match enumerated Unity process for project — treating as stale`);
109
+ }
110
+ return proc ? proc.pid : null;
111
+ }
112
+ export const closeCommand = new Command('close')
113
+ .description('Gracefully terminate the Unity Editor instance running for a given project path')
114
+ .argument('[path]', 'Path to the Unity project (defaults to current directory)')
115
+ .option('--timeout <seconds>', `Polite-quit timeout in seconds (default: ${DEFAULT_TIMEOUT_SECONDS})`, String(DEFAULT_TIMEOUT_SECONDS))
116
+ .option('--force', 'Hard-kill the Editor if it does not exit within --timeout')
117
+ .action(async (positionalPath, options) => {
118
+ const projectPath = resolveCloseProjectPath(positionalPath, process.cwd());
119
+ verbose(`close invoked for project: ${projectPath}`);
120
+ if (!fs.existsSync(projectPath)) {
121
+ ui.error(`Project path does not exist: ${projectPath}`);
122
+ process.exit(1);
123
+ }
124
+ if (!isUnityProjectRoot(projectPath)) {
125
+ ui.error(`Not a Unity project root: ${projectPath}`);
126
+ process.exit(1);
127
+ }
128
+ const timeoutSeconds = parseTimeoutSeconds(options.timeout);
129
+ if (timeoutSeconds === null) {
130
+ ui.error(`Invalid --timeout value: "${options.timeout}". Must be a positive integer (seconds).`);
131
+ process.exit(1);
132
+ }
133
+ const platform = nodePlatform();
134
+ const pid = resolveEditorPid(projectPath, platform);
135
+ if (pid === null) {
136
+ ui.success(`no running Editor for project at ${projectPath}`);
137
+ process.exit(0);
138
+ }
139
+ ui.heading('Closing Unity Editor');
140
+ ui.label('Project', projectPath);
141
+ ui.label('PID', String(pid));
142
+ ui.label('Timeout', `${timeoutSeconds}s`);
143
+ ui.label('Force', options.force ? 'yes' : 'no');
144
+ ui.divider();
145
+ // TOCTOU window: between resolveEditorPid returning and the signal call
146
+ // below, the original Unity process could exit and the OS could reuse
147
+ // the same numeric PID for an unrelated process (more likely on POSIX,
148
+ // where PIDs cycle aggressively). We accept the residual risk — the
149
+ // lockfile cross-check above already narrows it, holding a lock across
150
+ // a spawned Editor's lifetime is impractical, and the signals we send
151
+ // (SIGTERM / WM_CLOSE) are best-effort, not destructive.
152
+ const spinner = ui.startSpinner(`Sending polite-quit to PID ${pid}...`);
153
+ const sent = sendGracefulShutdown(pid, platform);
154
+ if (!sent) {
155
+ spinner.error(`Failed to deliver polite-quit signal to PID ${pid}`);
156
+ process.exit(1);
157
+ }
158
+ spinner.text = `Waiting up to ${timeoutSeconds}s for PID ${pid} to exit...`;
159
+ const timeoutMs = timeoutSeconds * 1000;
160
+ const exited = await waitForExit(pid, timeoutMs, platform);
161
+ if (exited) {
162
+ spinner.success(`Unity Editor (PID ${pid}) exited cleanly`);
163
+ process.exit(0);
164
+ }
165
+ if (!options.force) {
166
+ spinner.error(`Unity Editor (PID ${pid}) did not exit within ${timeoutSeconds}s. ` +
167
+ `Re-run with --force to hard-kill, or close it manually.`);
168
+ process.exit(1);
169
+ }
170
+ spinner.text = `Force-killing PID ${pid}...`;
171
+ const killed = sendForceKill(pid, platform);
172
+ if (!killed) {
173
+ spinner.error(`Failed to force-kill PID ${pid}`);
174
+ process.exit(1);
175
+ }
176
+ // Brief secondary wait so we report a true terminal state — the OS may
177
+ // need a moment to reap the process even after SIGKILL / `taskkill /F`.
178
+ const reaped = await waitForExit(pid, 5000, platform);
179
+ if (reaped) {
180
+ spinner.success(`Unity Editor (PID ${pid}) force-killed`);
181
+ process.exit(0);
182
+ }
183
+ spinner.error(`Unity Editor (PID ${pid}) is still alive after force-kill`);
184
+ process.exit(1);
185
+ });
186
+ //# sourceMappingURL=close.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"close.js","sourceRoot":"","sources":["../../src/commands/close.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander';
2
2
  import { createRequire } from 'module';
3
3
  import { bootstrapLocalCommand } from './commands/bootstrap-local.js';
4
+ import { closeCommand } from './commands/close.js';
4
5
  import { createProjectCommand } from './commands/create-project.js';
5
6
  import { installUnityCommand } from './commands/install-unity.js';
6
7
  import { openCommand } from './commands/open.js';
@@ -28,6 +29,7 @@ program
28
29
  // Register all subcommands
29
30
  const subcommands = [
30
31
  bootstrapLocalCommand,
32
+ closeCommand,
31
33
  configureCommand,
32
34
  createProjectCommand,
33
35
  installPluginCommand,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Supported Node.js `process.platform` values for the close subcommand.
3
+ * Narrowed alias of NodeJS.Platform — keeps signal-selection logic exhaustive
4
+ * in tests without forcing callers to import a Node-internal type.
5
+ */
6
+ export type SupportedPlatform = 'win32' | 'darwin' | 'linux';
7
+ /**
8
+ * The polite-quit "graceful shutdown" signal for the current platform.
9
+ *
10
+ * - Linux / macOS: `SIGTERM` — Unity catches this and runs its normal exit
11
+ * path (autosave, asset-import finalisation, plugin disconnect).
12
+ * - Windows: `WM_CLOSE` via `taskkill` (no `/F`) — same idea, sent through
13
+ * `taskkill /PID <pid>` in the runtime helper. We return the string
14
+ * sentinel `WM_CLOSE` here so the call site can branch on it.
15
+ *
16
+ * Exposed as a pure function so the unit test suite can assert the platform
17
+ * matrix without booting an editor.
18
+ */
19
+ export declare function gracefulShutdownSignal(platform: SupportedPlatform): NodeJS.Signals | 'WM_CLOSE';
20
+ /**
21
+ * The hard-kill signal for the current platform.
22
+ *
23
+ * - Linux / macOS: `SIGKILL`.
24
+ * - Windows: `taskkill /F` — represented here by the sentinel `TASKKILL_FORCE`.
25
+ */
26
+ export declare function forceKillSignal(platform: SupportedPlatform): NodeJS.Signals | 'TASKKILL_FORCE';
27
+ /**
28
+ * Read the editor PID stored in `<project>/Temp/UnityLockfile`.
29
+ *
30
+ * Unity writes the running editor's PID into the first 4 bytes of this file
31
+ * as a little-endian uint32 the moment it acquires the project lock. Returns
32
+ * `null` when the lockfile is missing, too small, or holds a zero/negative PID.
33
+ *
34
+ * The caller is responsible for cross-checking the PID against a live process
35
+ * list — stale lockfiles can survive an unclean editor exit.
36
+ */
37
+ export declare function readLockfilePid(projectPath: string): number | null;
38
+ /**
39
+ * Returns true when a process with the given PID is alive.
40
+ *
41
+ * Both platforms first try `process.kill(pid, 0)` — Node maps it through
42
+ * `OpenProcess` on Windows, so the check is in-process and effectively free
43
+ * compared to spawning `tasklist`. At a 250ms `waitForExit` cadence with a
44
+ * 30s default timeout that's ~120 fewer subprocesses per close on Windows.
45
+ *
46
+ * The Windows fallback to `tasklist` is kept as defence-in-depth: if a
47
+ * future Node release ever changes behaviour or `OpenProcess` denies access
48
+ * for a process owned by another session, we still get the right answer
49
+ * (just more slowly).
50
+ *
51
+ * On any failure the function errs on the side of "not running" and returns
52
+ * false, which keeps the wait loop progressing rather than spinning forever
53
+ * on a transient error.
54
+ */
55
+ export declare function isProcessAlive(pid: number, platform?: SupportedPlatform): boolean;
56
+ /**
57
+ * Send the platform's polite-quit signal to the given PID.
58
+ *
59
+ * Returns true on apparent success (the OS accepted the signal); false if the
60
+ * underlying call threw. The caller still has to poll `isProcessAlive` — the
61
+ * editor may take seconds to wind down.
62
+ *
63
+ * Windows caveat: `taskkill /PID <pid>` (no `/F`) delivers `WM_CLOSE`, which
64
+ * only reaches processes that own a top-level window on the same desktop /
65
+ * session as the caller. In headless contexts (Unity launched in session 0
66
+ * by a Windows service, or another non-interactive desktop) `taskkill` will
67
+ * exit 0 and this function will report `true`, but the editor will never
68
+ * receive the message. The `--timeout` will then elapse, and `--force` is
69
+ * the only path that brings the process down. This is documented in the
70
+ * close subcommand README and is consistent with the design decision to
71
+ * stick to OS-level polite-quit (rather than a Unity-side MCP `editor-quit`
72
+ * tool which would route through SignalR and bypass session walls).
73
+ */
74
+ export declare function sendGracefulShutdown(pid: number, platform?: SupportedPlatform): boolean;
75
+ /**
76
+ * Hard-kill the given PID. Used only when `--force` is set and the polite
77
+ * shutdown timed out.
78
+ */
79
+ export declare function sendForceKill(pid: number, platform?: SupportedPlatform): boolean;
80
+ /**
81
+ * Poll until the given PID disappears or the timeout elapses.
82
+ *
83
+ * Returns true if the process exited within `timeoutMs`; false otherwise.
84
+ * Uses a 250ms poll interval — short enough to give crisp UX on a fast exit
85
+ * without busy-looping.
86
+ */
87
+ export declare function waitForExit(pid: number, timeoutMs: number, platform?: SupportedPlatform): Promise<boolean>;
@@ -0,0 +1,241 @@
1
+ import { execFileSync } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import { platform as nodePlatform } from 'os';
5
+ import { verbose } from './ui.js';
6
+ /**
7
+ * The polite-quit "graceful shutdown" signal for the current platform.
8
+ *
9
+ * - Linux / macOS: `SIGTERM` — Unity catches this and runs its normal exit
10
+ * path (autosave, asset-import finalisation, plugin disconnect).
11
+ * - Windows: `WM_CLOSE` via `taskkill` (no `/F`) — same idea, sent through
12
+ * `taskkill /PID <pid>` in the runtime helper. We return the string
13
+ * sentinel `WM_CLOSE` here so the call site can branch on it.
14
+ *
15
+ * Exposed as a pure function so the unit test suite can assert the platform
16
+ * matrix without booting an editor.
17
+ */
18
+ export function gracefulShutdownSignal(platform) {
19
+ if (platform === 'win32')
20
+ return 'WM_CLOSE';
21
+ return 'SIGTERM';
22
+ }
23
+ /**
24
+ * The hard-kill signal for the current platform.
25
+ *
26
+ * - Linux / macOS: `SIGKILL`.
27
+ * - Windows: `taskkill /F` — represented here by the sentinel `TASKKILL_FORCE`.
28
+ */
29
+ export function forceKillSignal(platform) {
30
+ if (platform === 'win32')
31
+ return 'TASKKILL_FORCE';
32
+ return 'SIGKILL';
33
+ }
34
+ /**
35
+ * Read the editor PID stored in `<project>/Temp/UnityLockfile`.
36
+ *
37
+ * Unity writes the running editor's PID into the first 4 bytes of this file
38
+ * as a little-endian uint32 the moment it acquires the project lock. Returns
39
+ * `null` when the lockfile is missing, too small, or holds a zero/negative PID.
40
+ *
41
+ * The caller is responsible for cross-checking the PID against a live process
42
+ * list — stale lockfiles can survive an unclean editor exit.
43
+ */
44
+ export function readLockfilePid(projectPath) {
45
+ const lockfilePath = path.join(projectPath, 'Temp', 'UnityLockfile');
46
+ if (!fs.existsSync(lockfilePath)) {
47
+ verbose(`No Unity lockfile at ${lockfilePath}`);
48
+ return null;
49
+ }
50
+ let buf;
51
+ try {
52
+ buf = fs.readFileSync(lockfilePath);
53
+ }
54
+ catch (err) {
55
+ verbose(`Failed to read Unity lockfile: ${err instanceof Error ? err.message : String(err)}`);
56
+ return null;
57
+ }
58
+ if (buf.length < 4) {
59
+ verbose(`Unity lockfile too small (${buf.length} bytes)`);
60
+ return null;
61
+ }
62
+ // `readUInt32LE` always returns a finite integer in [0, 2^32-1], so a
63
+ // non-positive check is the only meaningful filter here.
64
+ const pid = buf.readUInt32LE(0);
65
+ if (pid <= 0) {
66
+ verbose(`Unity lockfile holds non-positive PID: ${pid}`);
67
+ return null;
68
+ }
69
+ return pid;
70
+ }
71
+ /**
72
+ * Returns true when a process with the given PID is alive.
73
+ *
74
+ * Both platforms first try `process.kill(pid, 0)` — Node maps it through
75
+ * `OpenProcess` on Windows, so the check is in-process and effectively free
76
+ * compared to spawning `tasklist`. At a 250ms `waitForExit` cadence with a
77
+ * 30s default timeout that's ~120 fewer subprocesses per close on Windows.
78
+ *
79
+ * The Windows fallback to `tasklist` is kept as defence-in-depth: if a
80
+ * future Node release ever changes behaviour or `OpenProcess` denies access
81
+ * for a process owned by another session, we still get the right answer
82
+ * (just more slowly).
83
+ *
84
+ * On any failure the function errs on the side of "not running" and returns
85
+ * false, which keeps the wait loop progressing rather than spinning forever
86
+ * on a transient error.
87
+ */
88
+ export function isProcessAlive(pid, platform = nodePlatform()) {
89
+ if (!Number.isFinite(pid) || pid <= 0)
90
+ return false;
91
+ try {
92
+ process.kill(pid, 0);
93
+ return true;
94
+ }
95
+ catch (err) {
96
+ const code = err.code;
97
+ // EPERM means the process exists but we can't signal it — treat as alive.
98
+ if (code === 'EPERM')
99
+ return true;
100
+ if (code !== 'ESRCH') {
101
+ // Anything other than ESRCH is unexpected; fall through to the
102
+ // Windows-only verification path so we don't return a wrong answer
103
+ // on a transient OS hiccup. POSIX paths just return false.
104
+ if (platform !== 'win32')
105
+ return false;
106
+ }
107
+ }
108
+ if (platform !== 'win32')
109
+ return false;
110
+ // Windows fallback: `tasklist /FI "PID eq <pid>" /NH /FO CSV`.
111
+ // The first CSV column is the image name, the second is the PID. Parse
112
+ // field-wise so a digit run that happens to appear in another column
113
+ // (image name, session, memory, status) cannot produce a false positive.
114
+ try {
115
+ const out = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'], {
116
+ encoding: 'utf-8',
117
+ timeout: 3000,
118
+ stdio: ['pipe', 'pipe', 'pipe'],
119
+ });
120
+ // tasklist prints "INFO: No tasks are running ..." when nothing matches.
121
+ const target = `"${pid}"`;
122
+ for (const line of out.split(/\r?\n/)) {
123
+ const trimmed = line.trim();
124
+ if (trimmed.length === 0)
125
+ continue;
126
+ // Second CSV field — split on `,` and check column index 1. CSV from
127
+ // tasklist quotes every field, so a simple split is safe. Image names
128
+ // with embedded commas would shift the PID into a later column and
129
+ // produce a false negative; that's an accepted trade-off — Unity
130
+ // executable names don't contain commas in practice, and any false
131
+ // negative simply ends the wait loop one tick early.
132
+ const cols = trimmed.split(',');
133
+ if (cols.length >= 2 && cols[1] === target)
134
+ return true;
135
+ }
136
+ return false;
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
142
+ /**
143
+ * Send the platform's polite-quit signal to the given PID.
144
+ *
145
+ * Returns true on apparent success (the OS accepted the signal); false if the
146
+ * underlying call threw. The caller still has to poll `isProcessAlive` — the
147
+ * editor may take seconds to wind down.
148
+ *
149
+ * Windows caveat: `taskkill /PID <pid>` (no `/F`) delivers `WM_CLOSE`, which
150
+ * only reaches processes that own a top-level window on the same desktop /
151
+ * session as the caller. In headless contexts (Unity launched in session 0
152
+ * by a Windows service, or another non-interactive desktop) `taskkill` will
153
+ * exit 0 and this function will report `true`, but the editor will never
154
+ * receive the message. The `--timeout` will then elapse, and `--force` is
155
+ * the only path that brings the process down. This is documented in the
156
+ * close subcommand README and is consistent with the design decision to
157
+ * stick to OS-level polite-quit (rather than a Unity-side MCP `editor-quit`
158
+ * tool which would route through SignalR and bypass session walls).
159
+ */
160
+ export function sendGracefulShutdown(pid, platform = nodePlatform()) {
161
+ if (!Number.isFinite(pid) || pid <= 0)
162
+ return false;
163
+ const sig = gracefulShutdownSignal(platform);
164
+ verbose(`Sending graceful shutdown to PID ${pid} via ${sig}`);
165
+ try {
166
+ if (sig === 'WM_CLOSE') {
167
+ execFileSync('taskkill', ['/PID', String(pid)], {
168
+ timeout: 3000,
169
+ stdio: ['pipe', 'pipe', 'pipe'],
170
+ });
171
+ return true;
172
+ }
173
+ process.kill(pid, sig);
174
+ return true;
175
+ }
176
+ catch (err) {
177
+ // Race: the editor can exit between PID resolution and signal delivery.
178
+ // POSIX surfaces this as ESRCH; Windows `taskkill` exits non-zero with
179
+ // "process … not found". The desired end state (process gone) is already
180
+ // reached, so we re-check liveness and report success when the PID is
181
+ // truly absent. This preserves close's idempotency contract.
182
+ if (!isProcessAlive(pid, platform)) {
183
+ verbose(`Graceful shutdown: PID ${pid} already gone — treating as success`);
184
+ return true;
185
+ }
186
+ verbose(`Graceful shutdown failed: ${err instanceof Error ? err.message : String(err)}`);
187
+ return false;
188
+ }
189
+ }
190
+ /**
191
+ * Hard-kill the given PID. Used only when `--force` is set and the polite
192
+ * shutdown timed out.
193
+ */
194
+ export function sendForceKill(pid, platform = nodePlatform()) {
195
+ if (!Number.isFinite(pid) || pid <= 0)
196
+ return false;
197
+ const sig = forceKillSignal(platform);
198
+ verbose(`Force-killing PID ${pid} via ${sig}`);
199
+ try {
200
+ if (sig === 'TASKKILL_FORCE') {
201
+ execFileSync('taskkill', ['/F', '/PID', String(pid)], {
202
+ timeout: 3000,
203
+ stdio: ['pipe', 'pipe', 'pipe'],
204
+ });
205
+ return true;
206
+ }
207
+ process.kill(pid, sig);
208
+ return true;
209
+ }
210
+ catch (err) {
211
+ // Same race as sendGracefulShutdown — the polite-quit path may have
212
+ // already won between waitForExit's last poll and our SIGKILL call.
213
+ if (!isProcessAlive(pid, platform)) {
214
+ verbose(`Force kill: PID ${pid} already gone — treating as success`);
215
+ return true;
216
+ }
217
+ verbose(`Force kill failed: ${err instanceof Error ? err.message : String(err)}`);
218
+ return false;
219
+ }
220
+ }
221
+ /**
222
+ * Poll until the given PID disappears or the timeout elapses.
223
+ *
224
+ * Returns true if the process exited within `timeoutMs`; false otherwise.
225
+ * Uses a 250ms poll interval — short enough to give crisp UX on a fast exit
226
+ * without busy-looping.
227
+ */
228
+ export async function waitForExit(pid, timeoutMs, platform = nodePlatform()) {
229
+ const pollIntervalMs = 250;
230
+ const deadline = Date.now() + timeoutMs;
231
+ while (Date.now() < deadline) {
232
+ if (!isProcessAlive(pid, platform)) {
233
+ return true;
234
+ }
235
+ await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
236
+ }
237
+ // One last check after the deadline elapses — the process may have exited
238
+ // in the trailing poll-interval slice we slept through.
239
+ return !isProcessAlive(pid, platform);
240
+ }
241
+ //# sourceMappingURL=unity-shutdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unity-shutdown.js","sourceRoot":"","sources":["../../src/utils/unity-shutdown.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unity-mcp-cli",
3
- "version": "0.66.1",
3
+ "version": "0.67.0",
4
4
  "description": "Cross-platform CLI tool for AI Game Developer (Skills & MCP). Full AI develop and test loop. Efficient token usage, advanced tools. Creates Unity project, installs plugins, configures tools, and manages HTTP connection with Unity Editor and a game made with Unity. Works with Claude Code, Gemini, Copilot, Cursor and any other absolutely for free.",
5
5
  "type": "module",
6
6
  "main": "dist/lib.js",