unity-mcp-cli 0.66.0 → 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/CHANGELOG.md +60 -0
- package/README.md +73 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +11 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/bootstrap-local.d.ts +17 -0
- package/dist/commands/bootstrap-local.js +83 -0
- package/dist/commands/bootstrap-local.js.map +1 -0
- package/dist/commands/close.d.ts +56 -0
- package/dist/commands/close.js +186 -0
- package/dist/commands/close.js.map +1 -0
- package/dist/commands/configure.js +37 -45
- package/dist/commands/configure.js.map +1 -1
- package/dist/commands/install-plugin.js +41 -23
- package/dist/commands/install-plugin.js.map +1 -1
- package/dist/commands/remove-plugin.js +21 -9
- package/dist/commands/remove-plugin.js.map +1 -1
- package/dist/commands/setup-mcp.js +32 -81
- package/dist/commands/setup-mcp.js.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/configure.d.ts +10 -0
- package/dist/lib/configure.js +85 -0
- package/dist/lib/configure.js.map +1 -0
- package/dist/lib/install-plugin.d.ts +8 -0
- package/dist/lib/install-plugin.js +68 -0
- package/dist/lib/install-plugin.js.map +1 -0
- package/dist/lib/logger.d.ts +7 -0
- package/dist/lib/logger.js +18 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/progress.d.ts +7 -0
- package/dist/lib/progress.js +16 -0
- package/dist/lib/progress.js.map +1 -0
- package/dist/lib/remove-plugin.d.ts +7 -0
- package/dist/lib/remove-plugin.js +51 -0
- package/dist/lib/remove-plugin.js.map +1 -0
- package/dist/lib/setup-mcp.d.ts +12 -0
- package/dist/lib/setup-mcp.js +141 -0
- package/dist/lib/setup-mcp.js.map +1 -0
- package/dist/lib/types.d.ts +132 -0
- package/dist/lib/types.js +8 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/lib/validation.d.ts +37 -0
- package/dist/lib/validation.js +50 -0
- package/dist/lib/validation.js.map +1 -0
- package/dist/lib.d.ts +5 -0
- package/dist/lib.js +18 -0
- package/dist/lib.js.map +1 -0
- package/dist/utils/manifest.d.ts +31 -3
- package/dist/utils/manifest.js +27 -11
- package/dist/utils/manifest.js.map +1 -1
- package/dist/utils/unity-shutdown.d.ts +87 -0
- package/dist/utils/unity-shutdown.js +241 -0
- package/dist/utils/unity-shutdown.js.map +1 -0
- package/package.json +20 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `unity-mcp-cli` will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.67.0] - 2026-04-21
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Library API.** The package now exposes its core commands as a typed,
|
|
13
|
+
side-effect-free library alongside the existing CLI binary.
|
|
14
|
+
Consumers can do:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { installPlugin, removePlugin, configure, setupMcp } from 'unity-mcp-cli';
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Available functions:
|
|
21
|
+
- `installPlugin(opts)` — install the Unity-MCP plugin into a Unity project
|
|
22
|
+
- `removePlugin(opts)` — remove the Unity-MCP plugin from a Unity project
|
|
23
|
+
- `configure(opts)` — enable/disable MCP tools, prompts, resources
|
|
24
|
+
- `setupMcp(opts)` — write an agent MCP config file
|
|
25
|
+
- `listAgentIds()` — list every supported agent id
|
|
26
|
+
|
|
27
|
+
All functions return a typed `{ success, ... }` result (never throw past the
|
|
28
|
+
public boundary) and accept an optional `onProgress(event)` callback that
|
|
29
|
+
fires for `start`, `dependencies-resolved`, `manifest-patched`, and
|
|
30
|
+
`done` phases.
|
|
31
|
+
|
|
32
|
+
- **`package.json` `exports` field.** The package now ships with a
|
|
33
|
+
conditional-exports map:
|
|
34
|
+
- `"."` — the library entry (`dist/lib.js`)
|
|
35
|
+
- `"./cli"` — the CLI entry (`dist/cli.js`), identical behaviour to
|
|
36
|
+
the `unity-mcp-cli` binary
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- Command handlers (`install-plugin`, `remove-plugin`, `configure`,
|
|
41
|
+
`setup-mcp`) are now thin Commander wrappers that delegate to the
|
|
42
|
+
library functions. Terminal output and exit codes are unchanged from
|
|
43
|
+
the user's perspective.
|
|
44
|
+
|
|
45
|
+
- `utils/manifest.ts` functions now accept an optional `logger` argument
|
|
46
|
+
so the library can run silently while the CLI preserves its chalk-
|
|
47
|
+
styled output. `addPluginToManifest` and `removePluginFromManifest`
|
|
48
|
+
also return structured result objects; return values are purely
|
|
49
|
+
additive and the existing call sites are unaffected.
|
|
50
|
+
|
|
51
|
+
### Notes
|
|
52
|
+
|
|
53
|
+
- No breaking changes to the CLI binary. `unity-mcp-cli install-plugin
|
|
54
|
+
./MyProject` and friends still work exactly as before.
|
|
55
|
+
- The library entry point has no top-level side effects: importing
|
|
56
|
+
`unity-mcp-cli` never writes to stdout/stderr and never parses argv.
|
|
57
|
+
An `import 'unity-mcp-cli'` or dynamic `import('unity-mcp-cli')`
|
|
58
|
+
call is safe to do anywhere.
|
|
59
|
+
|
|
60
|
+
[0.67.0]: https://github.com/IvanMurzak/Unity-MCP/releases/tag/cli-0.67.0
|
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
|

|
|
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
|
+

|
|
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).
|
|
@@ -557,3 +600,33 @@ Commands that manage editors or create projects use the **Unity Hub CLI** (`--he
|
|
|
557
600
|
> For the full Unity-MCP project documentation, see the [main README](https://github.com/IvanMurzak/Unity-MCP/blob/main/README.md).
|
|
558
601
|
|
|
559
602
|

|
|
603
|
+
|
|
604
|
+
# Library API (v0.67.0+)
|
|
605
|
+
|
|
606
|
+
In addition to the CLI binary, `unity-mcp-cli` exposes its core commands as a typed, side-effect-free library so other Node.js / TypeScript tools can embed the same install / configure flow without shelling out.
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
import { installPlugin, removePlugin, configure, setupMcp } from 'unity-mcp-cli';
|
|
610
|
+
|
|
611
|
+
const result = await installPlugin({
|
|
612
|
+
unityProjectPath: './MyUnityProject',
|
|
613
|
+
// version: '0.67.0', // optional — defaults to latest from OpenUPM
|
|
614
|
+
onProgress: (event) => {
|
|
615
|
+
// phase is one of: 'start' | 'dependencies-resolved' | 'manifest-patched' | 'done'
|
|
616
|
+
console.log(event.phase, event.message);
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
if (!result.success) {
|
|
621
|
+
console.error('Install failed:', result.error?.message);
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
console.log(`Installed v${result.installedVersion}`);
|
|
626
|
+
for (const warning of result.warnings) console.warn(warning);
|
|
627
|
+
for (const step of result.nextSteps) console.log(step);
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Each function returns a typed `{ success, ... }` result object; errors are never thrown past the public boundary. The library entry has no top-level side effects — `import 'unity-mcp-cli'` never parses argv and never writes to stdout or stderr.
|
|
631
|
+
|
|
632
|
+
See [`CHANGELOG.md`](CHANGELOG.md#0670---2026-04-21) for the full list of exported functions and types.
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './index.js';
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// CLI entry for `unity-mcp-cli`, exported at the `./cli` subpath.
|
|
2
|
+
//
|
|
3
|
+
// Importing this file runs the Commander-based CLI exactly as the
|
|
4
|
+
// `unity-mcp-cli` binary does — it parses argv, dispatches subcommands,
|
|
5
|
+
// and exits the process.
|
|
6
|
+
//
|
|
7
|
+
// Consumers that want the programmatic, side-effect-free surface
|
|
8
|
+
// should import the package root (`import { installPlugin } from
|
|
9
|
+
// "unity-mcp-cli"`) — NOT this file.
|
|
10
|
+
import './index.js';
|
|
11
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,EAAE;AACF,kEAAkE;AAClE,wEAAwE;AACxE,yBAAyB;AACzB,EAAE;AACF,iEAAiE;AACjE,iEAAiE;AACjE,qCAAqC;AAErC,OAAO,YAAY,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { type UnityConnectionConfig } from '../utils/config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Produce a new config object with local-mode fields pinned to `url` / `token`.
|
|
5
|
+
* Preserves all other keys from `current` (immutable update — does not mutate `current`).
|
|
6
|
+
*
|
|
7
|
+
* The plugin-side ConnectionMode enum only accepts "Custom" (local) or "Cloud".
|
|
8
|
+
* We intentionally normalize any integer (0 → Custom, 1 → Cloud) representation
|
|
9
|
+
* to the canonical string "Custom" so the on-disk file is explicit.
|
|
10
|
+
*/
|
|
11
|
+
export declare function pinLocalModeConfig(current: Readonly<UnityConnectionConfig>, url: string, token: string): UnityConnectionConfig;
|
|
12
|
+
/**
|
|
13
|
+
* Returns true iff `current` already reflects local mode with matching url + token.
|
|
14
|
+
* Used to make bootstrap-local a no-op when nothing would change on disk.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isAlreadyPinned(current: Readonly<UnityConnectionConfig>, url: string, token: string): boolean;
|
|
17
|
+
export declare const bootstrapLocalCommand: Command;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Copyright (c) 2025 Ivan Murzak. All rights reserved.
|
|
2
|
+
// Licensed under the Apache License, Version 2.0.
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as ui from '../utils/ui.js';
|
|
7
|
+
import { verbose } from '../utils/ui.js';
|
|
8
|
+
import { resolveAndValidateProjectPath } from '../utils/connection.js';
|
|
9
|
+
import { readConfig, writeConfig, } from '../utils/config.js';
|
|
10
|
+
/**
|
|
11
|
+
* Produce a new config object with local-mode fields pinned to `url` / `token`.
|
|
12
|
+
* Preserves all other keys from `current` (immutable update — does not mutate `current`).
|
|
13
|
+
*
|
|
14
|
+
* The plugin-side ConnectionMode enum only accepts "Custom" (local) or "Cloud".
|
|
15
|
+
* We intentionally normalize any integer (0 → Custom, 1 → Cloud) representation
|
|
16
|
+
* to the canonical string "Custom" so the on-disk file is explicit.
|
|
17
|
+
*/
|
|
18
|
+
export function pinLocalModeConfig(current, url, token) {
|
|
19
|
+
return {
|
|
20
|
+
...current,
|
|
21
|
+
connectionMode: 'Custom',
|
|
22
|
+
host: url,
|
|
23
|
+
token,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Returns true iff `current` already reflects local mode with matching url + token.
|
|
28
|
+
* Used to make bootstrap-local a no-op when nothing would change on disk.
|
|
29
|
+
*/
|
|
30
|
+
export function isAlreadyPinned(current, url, token) {
|
|
31
|
+
// Accept both "Custom" (string) and 0 (legacy int) as already-local.
|
|
32
|
+
// Anything else (including "Cloud", 1, or unset) means we still need to pin.
|
|
33
|
+
const mode = current.connectionMode;
|
|
34
|
+
if (mode !== 'Custom' && mode !== 0)
|
|
35
|
+
return false;
|
|
36
|
+
return current.host === url && current.token === token;
|
|
37
|
+
}
|
|
38
|
+
export const bootstrapLocalCommand = new Command('bootstrap-local')
|
|
39
|
+
.description('Pin a Unity project\'s MCP config to local mode with the given URL/token. Idempotent.')
|
|
40
|
+
.argument('[path]', 'Unity project path (defaults to cwd)')
|
|
41
|
+
.option('--path <path>', 'Unity project path (defaults to cwd)')
|
|
42
|
+
.requiredOption('--url <url>', 'Local MCP server URL to pin (e.g. http://localhost:5140/)')
|
|
43
|
+
.requiredOption('--token <token>', 'Local MCP bearer token to pin')
|
|
44
|
+
.option('--dry-run', 'Report what would change without writing to disk')
|
|
45
|
+
.action(async (positionalPath, options) => {
|
|
46
|
+
const projectPath = resolveAndValidateProjectPath(positionalPath, options);
|
|
47
|
+
// commander's requiredOption guarantees url/token are defined before the action runs.
|
|
48
|
+
const url = options.url.replace(/\/$/, '');
|
|
49
|
+
const token = options.token;
|
|
50
|
+
verbose(`Project path: ${projectPath}`);
|
|
51
|
+
verbose(`Target URL: ${url}`);
|
|
52
|
+
verbose(`Target token: ${'*'.repeat(Math.min(token.length, 8))}... (length ${token.length})`);
|
|
53
|
+
const configPath = path.join(projectPath, 'UserSettings', 'AI-Game-Developer-Config.json');
|
|
54
|
+
const existedBefore = fs.existsSync(configPath);
|
|
55
|
+
// Use readConfig (not getOrCreateConfig) so --dry-run truly has no side effects.
|
|
56
|
+
// When the file doesn't exist we pin onto an empty base; this mirrors a fresh
|
|
57
|
+
// worktree where the plugin has not yet written its default Cloud config.
|
|
58
|
+
const current = readConfig(projectPath) ?? {};
|
|
59
|
+
if (isAlreadyPinned(current, url, token)) {
|
|
60
|
+
ui.success(`MCP config already pinned to local mode (${url}). No changes written.`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const next = pinLocalModeConfig(current, url, token);
|
|
64
|
+
if (options.dryRun) {
|
|
65
|
+
ui.heading('bootstrap-local (dry run)');
|
|
66
|
+
ui.label('Config file', configPath);
|
|
67
|
+
ui.label('connectionMode', `${String(current.connectionMode ?? 'unset')} → Custom`);
|
|
68
|
+
ui.label('host', `${String(current.host ?? 'unset')} → ${url}`);
|
|
69
|
+
ui.label('token', `${current.token ? '<previous>' : '<unset>'} → <supplied>`);
|
|
70
|
+
ui.info('No changes written (--dry-run).');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
writeConfig(projectPath, next);
|
|
74
|
+
ui.heading('bootstrap-local');
|
|
75
|
+
ui.label('Config file', configPath);
|
|
76
|
+
ui.label('connectionMode', 'Custom');
|
|
77
|
+
ui.label('host', url);
|
|
78
|
+
ui.label('token', '<supplied>');
|
|
79
|
+
ui.success(existedBefore
|
|
80
|
+
? 'Config updated to pin local mode.'
|
|
81
|
+
: 'Config created and pinned to local mode.');
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=bootstrap-local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bootstrap-local.js","sourceRoot":"","sources":["../../src/commands/bootstrap-local.ts"],"names":[],"mappings":"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"}
|
|
@@ -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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"}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import { getOrCreateConfig, writeConfig, updateFeatures } from '../utils/config.js';
|
|
5
3
|
import * as ui from '../utils/ui.js';
|
|
6
4
|
import { verbose } from '../utils/ui.js';
|
|
5
|
+
import { configure } from '../lib/configure.js';
|
|
7
6
|
function parseCommaSeparated(value) {
|
|
8
7
|
return value.split(',').map((s) => s.trim()).filter(Boolean);
|
|
9
8
|
}
|
|
9
|
+
function buildAction(enable, disable, enableAll, disableAll) {
|
|
10
|
+
if (!enable && !disable && !enableAll && !disableAll)
|
|
11
|
+
return undefined;
|
|
12
|
+
return {
|
|
13
|
+
enableNames: enable,
|
|
14
|
+
disableNames: disable,
|
|
15
|
+
enableAll,
|
|
16
|
+
disableAll,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
10
19
|
export const configureCommand = new Command('configure')
|
|
11
20
|
.description('Configure MCP tools, prompts, and resources in AI-Game-Developer-Config.json')
|
|
12
21
|
.argument('[path]', 'Path to the Unity project')
|
|
@@ -31,63 +40,46 @@ export const configureCommand = new Command('configure')
|
|
|
31
40
|
process.exit(1);
|
|
32
41
|
}
|
|
33
42
|
const projectPath = path.resolve(resolvedPath);
|
|
34
|
-
|
|
35
|
-
|
|
43
|
+
verbose(`Loading config for project: ${projectPath}`);
|
|
44
|
+
// `--list` must be read-only: previously this command short-
|
|
45
|
+
// circuited before applying any enable/disable flags. Preserve
|
|
46
|
+
// that semantics by suppressing the mutating actions whenever
|
|
47
|
+
// `options.list` is set, regardless of other flags.
|
|
48
|
+
const toolsAction = buildAction(options.enableTools, options.disableTools, options.enableAllTools, options.disableAllTools);
|
|
49
|
+
const promptsAction = buildAction(options.enablePrompts, options.disablePrompts, options.enableAllPrompts, options.disableAllPrompts);
|
|
50
|
+
const resourcesAction = buildAction(options.enableResources, options.disableResources, options.enableAllResources, options.disableAllResources);
|
|
51
|
+
const result = await configure({
|
|
52
|
+
unityProjectPath: projectPath,
|
|
53
|
+
tools: options.list ? undefined : toolsAction,
|
|
54
|
+
prompts: options.list ? undefined : promptsAction,
|
|
55
|
+
resources: options.list ? undefined : resourcesAction,
|
|
56
|
+
});
|
|
57
|
+
if (!result.success) {
|
|
58
|
+
ui.error(result.error?.message ?? 'Unknown error');
|
|
36
59
|
process.exit(1);
|
|
37
60
|
}
|
|
38
|
-
|
|
39
|
-
const config = getOrCreateConfig(projectPath);
|
|
40
|
-
if (options.list) {
|
|
61
|
+
if (options.list && result.snapshot) {
|
|
41
62
|
ui.heading('Current configuration');
|
|
42
|
-
ui.label('Host',
|
|
43
|
-
ui.label('Keep Connected', String(
|
|
44
|
-
ui.label('Transport',
|
|
45
|
-
ui.label('Auth',
|
|
63
|
+
ui.label('Host', result.snapshot.host ?? 'not set');
|
|
64
|
+
ui.label('Keep Connected', String(result.snapshot.keepConnected ?? false));
|
|
65
|
+
ui.label('Transport', result.snapshot.transportMethod ?? 'streamableHttp');
|
|
66
|
+
ui.label('Auth', result.snapshot.authOption ?? 'none');
|
|
46
67
|
const printFeatures = (featureLabel, features) => {
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
ui.heading(featureLabel);
|
|
69
|
+
if (features.length === 0) {
|
|
49
70
|
ui.info('(none configured - all enabled by default)');
|
|
50
71
|
return;
|
|
51
72
|
}
|
|
52
|
-
ui.heading(featureLabel);
|
|
53
73
|
for (const f of features) {
|
|
54
74
|
ui.featureRow(f.name, f.enabled);
|
|
55
75
|
}
|
|
56
76
|
};
|
|
57
|
-
printFeatures('Tools',
|
|
58
|
-
printFeatures('Prompts',
|
|
59
|
-
printFeatures('Resources',
|
|
77
|
+
printFeatures('Tools', result.snapshot.tools);
|
|
78
|
+
printFeatures('Prompts', result.snapshot.prompts);
|
|
79
|
+
printFeatures('Resources', result.snapshot.resources);
|
|
60
80
|
return;
|
|
61
81
|
}
|
|
62
|
-
// Apply tool changes
|
|
63
|
-
if (options.enableTools || options.disableTools || options.enableAllTools || options.disableAllTools) {
|
|
64
|
-
updateFeatures(config, 'tools', {
|
|
65
|
-
enableNames: options.enableTools,
|
|
66
|
-
disableNames: options.disableTools,
|
|
67
|
-
enableAll: options.enableAllTools,
|
|
68
|
-
disableAll: options.disableAllTools,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
// Apply prompt changes
|
|
72
|
-
if (options.enablePrompts || options.disablePrompts || options.enableAllPrompts || options.disableAllPrompts) {
|
|
73
|
-
updateFeatures(config, 'prompts', {
|
|
74
|
-
enableNames: options.enablePrompts,
|
|
75
|
-
disableNames: options.disablePrompts,
|
|
76
|
-
enableAll: options.enableAllPrompts,
|
|
77
|
-
disableAll: options.disableAllPrompts,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
// Apply resource changes
|
|
81
|
-
if (options.enableResources || options.disableResources || options.enableAllResources || options.disableAllResources) {
|
|
82
|
-
updateFeatures(config, 'resources', {
|
|
83
|
-
enableNames: options.enableResources,
|
|
84
|
-
disableNames: options.disableResources,
|
|
85
|
-
enableAll: options.enableAllResources,
|
|
86
|
-
disableAll: options.disableAllResources,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
82
|
verbose('Writing updated configuration');
|
|
90
|
-
writeConfig(projectPath, config);
|
|
91
83
|
ui.success('Configuration updated successfully.');
|
|
92
84
|
});
|
|
93
85
|
//# sourceMappingURL=configure.js.map
|