uloop-cli 0.68.2 → 0.69.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 +87 -18
- package/README_ja.md +87 -18
- package/dist/cli.bundle.cjs +333 -137
- package/dist/cli.bundle.cjs.map +4 -4
- package/knip.json +4 -0
- package/package.json +4 -3
- package/src/__tests__/cli-e2e.test.ts +46 -4
- package/src/__tests__/execute-tool.test.ts +10 -0
- package/src/__tests__/port-resolver.test.ts +68 -0
- package/src/__tests__/project-validator.test.ts +107 -0
- package/src/arg-parser.ts +0 -118
- package/src/cli.ts +121 -14
- package/src/commands/focus-window.ts +54 -49
- package/src/compile-helpers.ts +5 -5
- package/src/default-tools.json +29 -1
- package/src/direct-unity-client.ts +2 -2
- package/src/execute-tool.ts +26 -0
- package/src/port-resolver.ts +39 -18
- package/src/project-root.ts +1 -1
- package/src/project-validator.ts +70 -0
- package/src/simple-framer.ts +2 -2
- package/src/skills/deprecated-skills.ts +1 -1
- package/src/skills/skills-manager.ts +9 -13
- package/src/skills/target-config.ts +1 -1
- package/src/spinner.ts +1 -1
- package/src/tool-cache.ts +18 -5
- package/src/version.ts +1 -1
package/src/cli.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from '
|
|
|
12
12
|
import { join, basename, dirname } from 'path';
|
|
13
13
|
import { homedir } from 'os';
|
|
14
14
|
import { spawn } from 'child_process';
|
|
15
|
-
import { Command } from 'commander';
|
|
15
|
+
import { Command, Option } from 'commander';
|
|
16
16
|
import {
|
|
17
17
|
executeToolCommand,
|
|
18
18
|
listAvailableTools,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
loadToolsCache,
|
|
25
25
|
hasCacheFile,
|
|
26
26
|
getDefaultTools,
|
|
27
|
+
getDefaultToolNames,
|
|
27
28
|
ToolDefinition,
|
|
28
29
|
ToolProperty,
|
|
29
30
|
getCachedServerVersion,
|
|
@@ -34,7 +35,8 @@ import { registerLaunchCommand } from './commands/launch.js';
|
|
|
34
35
|
import { registerFocusWindowCommand } from './commands/focus-window.js';
|
|
35
36
|
import { VERSION } from './version.js';
|
|
36
37
|
import { findUnityProjectRoot } from './project-root.js';
|
|
37
|
-
import { validateProjectPath } from './port-resolver.js';
|
|
38
|
+
import { validateProjectPath, UnityNotRunningError } from './port-resolver.js';
|
|
39
|
+
import { ProjectMismatchError } from './project-validator.js';
|
|
38
40
|
import { filterEnabledTools, isToolEnabled } from './tool-settings-loader.js';
|
|
39
41
|
|
|
40
42
|
interface CliOptions extends GlobalOptions {
|
|
@@ -45,6 +47,16 @@ const FOCUS_WINDOW_COMMAND = 'focus-window' as const;
|
|
|
45
47
|
const LAUNCH_COMMAND = 'launch' as const;
|
|
46
48
|
const UPDATE_COMMAND = 'update' as const;
|
|
47
49
|
|
|
50
|
+
const HELP_GROUP_BUILTIN_TOOLS = 'Built-in Tools:' as const;
|
|
51
|
+
const HELP_GROUP_THIRD_PARTY_TOOLS = 'Third-party Tools:' as const;
|
|
52
|
+
const HELP_GROUP_CLI_COMMANDS = 'CLI Commands:' as const;
|
|
53
|
+
|
|
54
|
+
const HELP_GROUP_ORDER = [
|
|
55
|
+
HELP_GROUP_CLI_COMMANDS,
|
|
56
|
+
HELP_GROUP_BUILTIN_TOOLS,
|
|
57
|
+
HELP_GROUP_THIRD_PARTY_TOOLS,
|
|
58
|
+
] as const;
|
|
59
|
+
|
|
48
60
|
// commander.js built-in flags that exit immediately without needing Unity
|
|
49
61
|
const NO_SYNC_FLAGS = ['-v', '--version', '-h', '--help'] as const;
|
|
50
62
|
|
|
@@ -66,7 +78,44 @@ program
|
|
|
66
78
|
.description('Unity MCP CLI - Direct communication with Unity Editor')
|
|
67
79
|
.version(VERSION, '-v, --version', 'Output the version number')
|
|
68
80
|
.showHelpAfterError('(run with -h for available options)')
|
|
69
|
-
.configureHelp({
|
|
81
|
+
.configureHelp({
|
|
82
|
+
sortSubcommands: true,
|
|
83
|
+
// commander.js default groupItems determines group display order by registration order,
|
|
84
|
+
// but CLI commands are registered at module level (before tools), so the default order
|
|
85
|
+
// would be wrong. We re-implement to enforce HELP_GROUP_ORDER.
|
|
86
|
+
groupItems<T extends Command | Option>(
|
|
87
|
+
unsortedItems: T[],
|
|
88
|
+
visibleItems: T[],
|
|
89
|
+
getGroup: (item: T) => string,
|
|
90
|
+
): Map<string, T[]> {
|
|
91
|
+
const groupMap = new Map<string, T[]>();
|
|
92
|
+
for (const item of unsortedItems) {
|
|
93
|
+
const group: string = getGroup(item);
|
|
94
|
+
if (!groupMap.has(group)) {
|
|
95
|
+
groupMap.set(group, []);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const item of visibleItems) {
|
|
99
|
+
const group: string = getGroup(item);
|
|
100
|
+
if (!groupMap.has(group)) {
|
|
101
|
+
groupMap.set(group, []);
|
|
102
|
+
}
|
|
103
|
+
groupMap.get(group)!.push(item);
|
|
104
|
+
}
|
|
105
|
+
const ordered = new Map<string, T[]>();
|
|
106
|
+
for (const key of HELP_GROUP_ORDER) {
|
|
107
|
+
const items: T[] | undefined = groupMap.get(key);
|
|
108
|
+
if (items !== undefined) {
|
|
109
|
+
ordered.set(key, items);
|
|
110
|
+
groupMap.delete(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const [key, value] of groupMap) {
|
|
114
|
+
ordered.set(key, value);
|
|
115
|
+
}
|
|
116
|
+
return ordered;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
70
119
|
|
|
71
120
|
// --list-commands: Output command names for shell completion
|
|
72
121
|
program.option('--list-commands', 'List all command names (for shell completion)');
|
|
@@ -74,6 +123,12 @@ program.option('--list-commands', 'List all command names (for shell completion)
|
|
|
74
123
|
// --list-options <cmd>: Output options for a specific command (for shell completion)
|
|
75
124
|
program.option('--list-options <cmd>', 'List options for a command (for shell completion)');
|
|
76
125
|
|
|
126
|
+
// Set default help group for CLI commands registered at module level
|
|
127
|
+
program.commandsGroup(HELP_GROUP_CLI_COMMANDS);
|
|
128
|
+
// Eagerly initialize the implicit help command so it inherits the CLI Commands group.
|
|
129
|
+
// Without this, the lazy-created help command skips _initCommandGroup and falls into "Commands:" group.
|
|
130
|
+
program.helpCommand(true);
|
|
131
|
+
|
|
77
132
|
// Built-in commands (not from tools.json)
|
|
78
133
|
program
|
|
79
134
|
.command('list')
|
|
@@ -132,12 +187,13 @@ registerLaunchCommand(program);
|
|
|
132
187
|
/**
|
|
133
188
|
* Register a tool as a CLI command dynamically.
|
|
134
189
|
*/
|
|
135
|
-
function registerToolCommand(tool: ToolDefinition): void {
|
|
190
|
+
function registerToolCommand(tool: ToolDefinition, helpGroup: string): void {
|
|
136
191
|
// Skip if already registered as a built-in command
|
|
137
192
|
if (BUILTIN_COMMANDS.includes(tool.name as (typeof BUILTIN_COMMANDS)[number])) {
|
|
138
193
|
return;
|
|
139
194
|
}
|
|
140
|
-
const
|
|
195
|
+
const firstLine: string = tool.description.split('\n')[0];
|
|
196
|
+
const cmd = program.command(tool.name).description(firstLine).helpGroup(helpGroup);
|
|
141
197
|
|
|
142
198
|
// Add options from inputSchema.properties
|
|
143
199
|
const properties = tool.inputSchema.properties;
|
|
@@ -304,6 +360,10 @@ function convertValue(value: unknown, propInfo: ToolProperty): unknown {
|
|
|
304
360
|
return value;
|
|
305
361
|
}
|
|
306
362
|
|
|
363
|
+
function getToolHelpGroup(toolName: string, defaultToolNames: ReadonlySet<string>): string {
|
|
364
|
+
return defaultToolNames.has(toolName) ? HELP_GROUP_BUILTIN_TOOLS : HELP_GROUP_THIRD_PARTY_TOOLS;
|
|
365
|
+
}
|
|
366
|
+
|
|
307
367
|
function extractGlobalOptions(options: Record<string, unknown>): GlobalOptions {
|
|
308
368
|
return {
|
|
309
369
|
port: options['port'] as string | undefined,
|
|
@@ -363,6 +423,28 @@ async function runWithErrorHandling(fn: () => Promise<void>): Promise<void> {
|
|
|
363
423
|
try {
|
|
364
424
|
await fn();
|
|
365
425
|
} catch (error) {
|
|
426
|
+
if (error instanceof UnityNotRunningError) {
|
|
427
|
+
console.error('\x1b[31mError: Unity Editor for this project is not running.\x1b[0m');
|
|
428
|
+
console.error('');
|
|
429
|
+
console.error(` Project: ${error.projectRoot}`);
|
|
430
|
+
console.error('');
|
|
431
|
+
console.error('Start the Unity Editor for this project and try again.');
|
|
432
|
+
process.exit(1);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (error instanceof ProjectMismatchError) {
|
|
436
|
+
console.error('\x1b[31mError: Unity Editor for this project is not running.\x1b[0m');
|
|
437
|
+
console.error('');
|
|
438
|
+
console.error(` Project: ${error.expectedProjectRoot}`);
|
|
439
|
+
console.error(` Connected to: ${error.connectedProjectRoot}`);
|
|
440
|
+
console.error('');
|
|
441
|
+
console.error('Another Unity instance was found, but it belongs to a different project.');
|
|
442
|
+
console.error(
|
|
443
|
+
'Start the Unity Editor for this project, or use --port to specify the target.',
|
|
444
|
+
);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
|
|
366
448
|
const message = error instanceof Error ? error.message : String(error);
|
|
367
449
|
|
|
368
450
|
// Unity busy states have clear causes - no version diagnostic needed
|
|
@@ -788,6 +870,26 @@ function shouldSkipAutoSync(cmdName: string | undefined, args: string[]): boolea
|
|
|
788
870
|
return args.some((arg) => (NO_SYNC_FLAGS as readonly string[]).includes(arg));
|
|
789
871
|
}
|
|
790
872
|
|
|
873
|
+
// Options that consume the next argument as a value
|
|
874
|
+
const OPTIONS_WITH_VALUE: ReadonlySet<string> = new Set(['--port', '-p', '--project-path']);
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Find the first non-option argument that is not a value of a known option.
|
|
878
|
+
*/
|
|
879
|
+
function findCommandName(args: readonly string[]): string | undefined {
|
|
880
|
+
for (let i = 0; i < args.length; i++) {
|
|
881
|
+
const arg: string = args[i];
|
|
882
|
+
if (arg.startsWith('-')) {
|
|
883
|
+
if (OPTIONS_WITH_VALUE.has(arg)) {
|
|
884
|
+
i++; // skip the next arg (option value)
|
|
885
|
+
}
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
return arg;
|
|
889
|
+
}
|
|
890
|
+
return undefined;
|
|
891
|
+
}
|
|
892
|
+
|
|
791
893
|
function extractSyncGlobalOptions(args: string[]): GlobalOptions {
|
|
792
894
|
const options: GlobalOptions = {};
|
|
793
895
|
|
|
@@ -832,8 +934,8 @@ async function main(): Promise<void> {
|
|
|
832
934
|
}
|
|
833
935
|
|
|
834
936
|
const args = process.argv.slice(2);
|
|
835
|
-
const cmdName = args.find((arg) => !arg.startsWith('-'));
|
|
836
937
|
const syncGlobalOptions = extractSyncGlobalOptions(args);
|
|
938
|
+
const cmdName = findCommandName(args);
|
|
837
939
|
|
|
838
940
|
// No command name = no Unity operation; skip project detection
|
|
839
941
|
const NO_PROJECT_COMMANDS = [UPDATE_COMMAND, 'completion'] as const;
|
|
@@ -841,20 +943,24 @@ async function main(): Promise<void> {
|
|
|
841
943
|
cmdName === undefined || (NO_PROJECT_COMMANDS as readonly string[]).includes(cmdName);
|
|
842
944
|
|
|
843
945
|
if (skipProjectDetection) {
|
|
844
|
-
const
|
|
946
|
+
const defaultToolNames: ReadonlySet<string> = getDefaultToolNames();
|
|
845
947
|
// Only filter disabled tools for top-level help (uloop --help); subcommand help
|
|
846
948
|
// (e.g. uloop completion --help) does not list dynamic tools, so scanning is unnecessary
|
|
847
949
|
const isTopLevelHelp: boolean =
|
|
848
950
|
cmdName === undefined && (args.includes('-h') || args.includes('--help'));
|
|
849
951
|
const shouldFilter: boolean = syncGlobalOptions.projectPath !== undefined || isTopLevelHelp;
|
|
952
|
+
// Use cache to include third-party tools in help output; falls back to defaults when no cache exists
|
|
953
|
+
const sourceTools: ToolDefinition[] = shouldFilter
|
|
954
|
+
? loadToolsCache(syncGlobalOptions.projectPath).tools
|
|
955
|
+
: getDefaultTools().tools;
|
|
850
956
|
const tools: ToolDefinition[] = shouldFilter
|
|
851
|
-
? filterEnabledTools(
|
|
852
|
-
:
|
|
957
|
+
? filterEnabledTools(sourceTools, syncGlobalOptions.projectPath)
|
|
958
|
+
: sourceTools;
|
|
853
959
|
if (!shouldFilter || isToolEnabled(FOCUS_WINDOW_COMMAND, syncGlobalOptions.projectPath)) {
|
|
854
|
-
registerFocusWindowCommand(program);
|
|
960
|
+
registerFocusWindowCommand(program, HELP_GROUP_BUILTIN_TOOLS);
|
|
855
961
|
}
|
|
856
962
|
for (const tool of tools) {
|
|
857
|
-
registerToolCommand(tool);
|
|
963
|
+
registerToolCommand(tool, getToolHelpGroup(tool.name, defaultToolNames));
|
|
858
964
|
}
|
|
859
965
|
program.parse();
|
|
860
966
|
return;
|
|
@@ -887,11 +993,12 @@ async function main(): Promise<void> {
|
|
|
887
993
|
// Register tool commands from cache (after potential auto-sync)
|
|
888
994
|
const toolsCache = loadToolsCache();
|
|
889
995
|
const projectPath: string | undefined = syncGlobalOptions.projectPath;
|
|
996
|
+
const defaultToolNames: ReadonlySet<string> = getDefaultToolNames();
|
|
890
997
|
if (isToolEnabled(FOCUS_WINDOW_COMMAND, projectPath)) {
|
|
891
|
-
registerFocusWindowCommand(program);
|
|
998
|
+
registerFocusWindowCommand(program, HELP_GROUP_BUILTIN_TOOLS);
|
|
892
999
|
}
|
|
893
1000
|
for (const tool of filterEnabledTools(toolsCache.tools, projectPath)) {
|
|
894
|
-
registerToolCommand(tool);
|
|
1001
|
+
registerToolCommand(tool, getToolHelpGroup(tool.name, defaultToolNames));
|
|
895
1002
|
}
|
|
896
1003
|
|
|
897
1004
|
if (cmdName && !commandExists(cmdName, projectPath)) {
|
|
@@ -906,7 +1013,7 @@ async function main(): Promise<void> {
|
|
|
906
1013
|
const newCache = loadToolsCache();
|
|
907
1014
|
const tool = filterEnabledTools(newCache.tools, projectPath).find((t) => t.name === cmdName);
|
|
908
1015
|
if (tool) {
|
|
909
|
-
registerToolCommand(tool);
|
|
1016
|
+
registerToolCommand(tool, getToolHelpGroup(tool.name, defaultToolNames));
|
|
910
1017
|
console.log(`\x1b[32m✓ Found '${cmdName}' after sync.\x1b[0m\n`);
|
|
911
1018
|
} else {
|
|
912
1019
|
console.error(`\x1b[31mError: Command '${cmdName}' not found even after sync.\x1b[0m`);
|
|
@@ -12,66 +12,71 @@ import { findRunningUnityProcess, focusUnityProcess } from 'launch-unity';
|
|
|
12
12
|
import { findUnityProjectRoot } from '../project-root.js';
|
|
13
13
|
import { validateProjectPath } from '../port-resolver.js';
|
|
14
14
|
|
|
15
|
-
export function registerFocusWindowCommand(program: Command): void {
|
|
16
|
-
program
|
|
15
|
+
export function registerFocusWindowCommand(program: Command, helpGroup?: string): void {
|
|
16
|
+
const cmd = program
|
|
17
17
|
.command('focus-window')
|
|
18
18
|
.description('Bring Unity Editor window to front using OS-level commands')
|
|
19
|
-
.option('--project-path <path>', 'Unity project path')
|
|
20
|
-
.action(async (options: { projectPath?: string }) => {
|
|
21
|
-
let projectRoot: string | null;
|
|
22
|
-
if (options.projectPath !== undefined) {
|
|
23
|
-
try {
|
|
24
|
-
projectRoot = validateProjectPath(options.projectPath);
|
|
25
|
-
} catch (error) {
|
|
26
|
-
console.error(
|
|
27
|
-
JSON.stringify({
|
|
28
|
-
Success: false,
|
|
29
|
-
Message: error instanceof Error ? error.message : String(error),
|
|
30
|
-
}),
|
|
31
|
-
);
|
|
32
|
-
process.exit(1);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
} else {
|
|
36
|
-
projectRoot = findUnityProjectRoot();
|
|
37
|
-
}
|
|
38
|
-
if (projectRoot === null) {
|
|
39
|
-
console.error(
|
|
40
|
-
JSON.stringify({
|
|
41
|
-
Success: false,
|
|
42
|
-
Message: 'Unity project not found',
|
|
43
|
-
}),
|
|
44
|
-
);
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
19
|
+
.option('--project-path <path>', 'Unity project path');
|
|
47
20
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
JSON.stringify({
|
|
52
|
-
Success: false,
|
|
53
|
-
Message: 'No running Unity process found for this project',
|
|
54
|
-
}),
|
|
55
|
-
);
|
|
56
|
-
process.exit(1);
|
|
57
|
-
}
|
|
21
|
+
if (helpGroup !== undefined) {
|
|
22
|
+
cmd.helpGroup(helpGroup);
|
|
23
|
+
}
|
|
58
24
|
|
|
25
|
+
cmd.action(async (options: { projectPath?: string }) => {
|
|
26
|
+
let projectRoot: string | null;
|
|
27
|
+
if (options.projectPath !== undefined) {
|
|
59
28
|
try {
|
|
60
|
-
|
|
61
|
-
console.log(
|
|
62
|
-
JSON.stringify({
|
|
63
|
-
Success: true,
|
|
64
|
-
Message: `Unity Editor window focused (PID: ${runningProcess.pid})`,
|
|
65
|
-
}),
|
|
66
|
-
);
|
|
29
|
+
projectRoot = validateProjectPath(options.projectPath);
|
|
67
30
|
} catch (error) {
|
|
68
31
|
console.error(
|
|
69
32
|
JSON.stringify({
|
|
70
33
|
Success: false,
|
|
71
|
-
Message:
|
|
34
|
+
Message: error instanceof Error ? error.message : String(error),
|
|
72
35
|
}),
|
|
73
36
|
);
|
|
74
37
|
process.exit(1);
|
|
38
|
+
return;
|
|
75
39
|
}
|
|
76
|
-
}
|
|
40
|
+
} else {
|
|
41
|
+
projectRoot = findUnityProjectRoot();
|
|
42
|
+
}
|
|
43
|
+
if (projectRoot === null) {
|
|
44
|
+
console.error(
|
|
45
|
+
JSON.stringify({
|
|
46
|
+
Success: false,
|
|
47
|
+
Message: 'Unity project not found',
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const runningProcess = await findRunningUnityProcess(projectRoot);
|
|
54
|
+
if (!runningProcess) {
|
|
55
|
+
console.error(
|
|
56
|
+
JSON.stringify({
|
|
57
|
+
Success: false,
|
|
58
|
+
Message: 'No running Unity process found for this project',
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await focusUnityProcess(runningProcess.pid);
|
|
66
|
+
console.log(
|
|
67
|
+
JSON.stringify({
|
|
68
|
+
Success: true,
|
|
69
|
+
Message: `Unity Editor window focused (PID: ${runningProcess.pid})`,
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
Success: false,
|
|
76
|
+
Message: `Failed to focus Unity window: ${error instanceof Error ? error.message : String(error)}`,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
77
82
|
}
|
package/src/compile-helpers.ts
CHANGED
|
@@ -17,14 +17,14 @@ import { join } from 'path';
|
|
|
17
17
|
// Only alphanumeric, underscore, and hyphen — blocks path separators and traversal sequences
|
|
18
18
|
const SAFE_REQUEST_ID_PATTERN: RegExp = /^[a-zA-Z0-9_-]+$/;
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const COMPILE_FORCE_RECOMPILE_ARG_KEYS = [
|
|
21
21
|
'ForceRecompile',
|
|
22
22
|
'forceRecompile',
|
|
23
23
|
'force_recompile',
|
|
24
24
|
'force-recompile',
|
|
25
25
|
] as const;
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const COMPILE_WAIT_FOR_DOMAIN_RELOAD_ARG_KEYS = [
|
|
28
28
|
'WaitForDomainReload',
|
|
29
29
|
'waitForDomainReload',
|
|
30
30
|
'wait_for_domain_reload',
|
|
@@ -36,7 +36,7 @@ export interface CompileExecutionOptions {
|
|
|
36
36
|
waitForDomainReload: boolean;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
interface CompileCompletionWaitOptions {
|
|
40
40
|
projectRoot: string;
|
|
41
41
|
requestId: string;
|
|
42
42
|
timeoutMs: number;
|
|
@@ -45,9 +45,9 @@ export interface CompileCompletionWaitOptions {
|
|
|
45
45
|
isUnityReadyWhenIdle?: () => Promise<boolean>;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
type CompileCompletionOutcome = 'completed' | 'timed_out';
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
interface CompileCompletionResult<T> {
|
|
51
51
|
outcome: CompileCompletionOutcome;
|
|
52
52
|
result?: T;
|
|
53
53
|
}
|
package/src/default-tools.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.69.0",
|
|
3
3
|
"tools": [
|
|
4
4
|
{
|
|
5
5
|
"name": "compile",
|
|
@@ -239,6 +239,34 @@
|
|
|
239
239
|
}
|
|
240
240
|
}
|
|
241
241
|
},
|
|
242
|
+
{
|
|
243
|
+
"name": "get-unity-search-providers",
|
|
244
|
+
"description": "Get detailed information about Unity Search providers including display names, descriptions, active status, and capabilities",
|
|
245
|
+
"inputSchema": {
|
|
246
|
+
"type": "object",
|
|
247
|
+
"properties": {
|
|
248
|
+
"ProviderId": {
|
|
249
|
+
"type": "string",
|
|
250
|
+
"description": "Specific provider ID to get details for (empty = all providers). Examples: 'asset', 'scene', 'menu', 'settings'"
|
|
251
|
+
},
|
|
252
|
+
"ActiveOnly": {
|
|
253
|
+
"type": "boolean",
|
|
254
|
+
"description": "Whether to include only active providers",
|
|
255
|
+
"default": false
|
|
256
|
+
},
|
|
257
|
+
"SortByPriority": {
|
|
258
|
+
"type": "boolean",
|
|
259
|
+
"description": "Sort providers by priority (lower number = higher priority)",
|
|
260
|
+
"default": true
|
|
261
|
+
},
|
|
262
|
+
"IncludeDescriptions": {
|
|
263
|
+
"type": "boolean",
|
|
264
|
+
"description": "Include detailed descriptions for each provider",
|
|
265
|
+
"default": true
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
},
|
|
242
270
|
{
|
|
243
271
|
"name": "get-menu-items",
|
|
244
272
|
"description": "Retrieve Unity MenuItems",
|
|
@@ -13,14 +13,14 @@ const JSONRPC_VERSION = '2.0';
|
|
|
13
13
|
const DEFAULT_HOST = '127.0.0.1';
|
|
14
14
|
const NETWORK_TIMEOUT_MS = 180000;
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
interface JsonRpcRequest {
|
|
17
17
|
jsonrpc: string;
|
|
18
18
|
method: string;
|
|
19
19
|
params?: Record<string, unknown>;
|
|
20
20
|
id: number;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
interface JsonRpcResponse {
|
|
24
24
|
jsonrpc: string;
|
|
25
25
|
result?: unknown;
|
|
26
26
|
error?: {
|
package/src/execute-tool.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { join } from 'path';
|
|
|
13
13
|
import * as semver from 'semver';
|
|
14
14
|
import { DirectUnityClient } from './direct-unity-client.js';
|
|
15
15
|
import { resolveUnityPort, validateProjectPath } from './port-resolver.js';
|
|
16
|
+
import { validateConnectedProject } from './project-validator.js';
|
|
16
17
|
import { saveToolsCache, getCacheFilePath, ToolsCache, ToolDefinition } from './tool-cache.js';
|
|
17
18
|
import { VERSION } from './version.js';
|
|
18
19
|
import { createSpinner } from './spinner.js';
|
|
@@ -214,6 +215,9 @@ export async function executeToolCommand(
|
|
|
214
215
|
? validateProjectPath(globalOptions.projectPath)
|
|
215
216
|
: findUnityProjectRoot();
|
|
216
217
|
|
|
218
|
+
// Validate project identity only when port was auto-resolved (not --port) and project root is known
|
|
219
|
+
const shouldValidateProject = portNumber === undefined && projectRoot !== null;
|
|
220
|
+
|
|
217
221
|
// Monotonically-increasing flag: once true, retries cannot reset it to false.
|
|
218
222
|
// The retry loop overwrites `lastError` and `immediateResult` on each attempt,
|
|
219
223
|
// which destroys the evidence of whether an earlier attempt successfully dispatched
|
|
@@ -229,6 +233,10 @@ export async function executeToolCommand(
|
|
|
229
233
|
try {
|
|
230
234
|
await client.connect();
|
|
231
235
|
|
|
236
|
+
if (shouldValidateProject) {
|
|
237
|
+
await validateConnectedProject(client, projectRoot);
|
|
238
|
+
}
|
|
239
|
+
|
|
232
240
|
spinner.update(`Executing ${toolName}...`);
|
|
233
241
|
// connect() succeeded: socket is established. sendRequest() calls socket.write()
|
|
234
242
|
// synchronously (direct-unity-client.ts:136), so the data reaches the kernel
|
|
@@ -383,6 +391,11 @@ export async function listAvailableTools(globalOptions: GlobalOptions): Promise<
|
|
|
383
391
|
portNumber = parsed;
|
|
384
392
|
}
|
|
385
393
|
const port = await resolveUnityPort(portNumber, globalOptions.projectPath);
|
|
394
|
+
const projectRoot =
|
|
395
|
+
globalOptions.projectPath !== undefined
|
|
396
|
+
? validateProjectPath(globalOptions.projectPath)
|
|
397
|
+
: findUnityProjectRoot();
|
|
398
|
+
const shouldValidateProject = portNumber === undefined && projectRoot !== null;
|
|
386
399
|
|
|
387
400
|
const restoreStdin = suppressStdinEcho();
|
|
388
401
|
const spinner = createSpinner('Connecting to Unity...');
|
|
@@ -395,6 +408,10 @@ export async function listAvailableTools(globalOptions: GlobalOptions): Promise<
|
|
|
395
408
|
try {
|
|
396
409
|
await client.connect();
|
|
397
410
|
|
|
411
|
+
if (shouldValidateProject) {
|
|
412
|
+
await validateConnectedProject(client, projectRoot);
|
|
413
|
+
}
|
|
414
|
+
|
|
398
415
|
spinner.update('Fetching tool list...');
|
|
399
416
|
const result = await client.sendRequest<{
|
|
400
417
|
Tools: Array<{ name: string; description: string }>;
|
|
@@ -471,6 +488,11 @@ export async function syncTools(globalOptions: GlobalOptions): Promise<void> {
|
|
|
471
488
|
portNumber = parsed;
|
|
472
489
|
}
|
|
473
490
|
const port = await resolveUnityPort(portNumber, globalOptions.projectPath);
|
|
491
|
+
const projectRoot =
|
|
492
|
+
globalOptions.projectPath !== undefined
|
|
493
|
+
? validateProjectPath(globalOptions.projectPath)
|
|
494
|
+
: findUnityProjectRoot();
|
|
495
|
+
const shouldValidateProject = portNumber === undefined && projectRoot !== null;
|
|
474
496
|
|
|
475
497
|
const restoreStdin = suppressStdinEcho();
|
|
476
498
|
const spinner = createSpinner('Connecting to Unity...');
|
|
@@ -483,6 +505,10 @@ export async function syncTools(globalOptions: GlobalOptions): Promise<void> {
|
|
|
483
505
|
try {
|
|
484
506
|
await client.connect();
|
|
485
507
|
|
|
508
|
+
if (shouldValidateProject) {
|
|
509
|
+
await validateConnectedProject(client, projectRoot);
|
|
510
|
+
}
|
|
511
|
+
|
|
486
512
|
spinner.update('Syncing tools...');
|
|
487
513
|
const result = await client.sendRequest<{
|
|
488
514
|
Tools: UnityToolInfo[];
|
package/src/port-resolver.ts
CHANGED
|
@@ -11,7 +11,11 @@ import { existsSync } from 'fs';
|
|
|
11
11
|
import { join, resolve } from 'path';
|
|
12
12
|
import { findUnityProjectRoot, isUnityProject, hasUloopInstalled } from './project-root.js';
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
export class UnityNotRunningError extends Error {
|
|
15
|
+
constructor(public readonly projectRoot: string) {
|
|
16
|
+
super('UNITY_NOT_RUNNING');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
15
19
|
|
|
16
20
|
interface UnityMcpSettings {
|
|
17
21
|
isServerRunning?: boolean;
|
|
@@ -78,11 +82,7 @@ export async function resolveUnityPort(
|
|
|
78
82
|
|
|
79
83
|
if (projectPath !== undefined) {
|
|
80
84
|
const resolved = validateProjectPath(projectPath);
|
|
81
|
-
|
|
82
|
-
if (settingsPort !== null) {
|
|
83
|
-
return settingsPort;
|
|
84
|
-
}
|
|
85
|
-
return DEFAULT_PORT;
|
|
85
|
+
return await readPortFromSettingsOrThrow(resolved);
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
const projectRoot = findUnityProjectRoot();
|
|
@@ -92,34 +92,55 @@ export async function resolveUnityPort(
|
|
|
92
92
|
);
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return settingsPort;
|
|
98
|
-
}
|
|
95
|
+
return await readPortFromSettingsOrThrow(projectRoot);
|
|
96
|
+
}
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
function createSettingsReadError(projectRoot: string): Error {
|
|
99
|
+
const settingsPath = join(projectRoot, 'UserSettings/UnityMcpSettings.json');
|
|
100
|
+
return new Error(
|
|
101
|
+
`Could not read Unity server port from settings.\n\n` +
|
|
102
|
+
` Settings file: ${settingsPath}\n\n` +
|
|
103
|
+
`Run 'uloop launch -r' to restart Unity, or use --port to specify the port directly.`,
|
|
104
|
+
);
|
|
101
105
|
}
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
// File I/O and JSON parsing can fail for external reasons (permissions, corruption, concurrent writes)
|
|
108
|
+
async function readPortFromSettingsOrThrow(projectRoot: string): Promise<number> {
|
|
104
109
|
const settingsPath = join(projectRoot, 'UserSettings/UnityMcpSettings.json');
|
|
105
110
|
|
|
106
111
|
if (!existsSync(settingsPath)) {
|
|
107
|
-
|
|
112
|
+
throw createSettingsReadError(projectRoot);
|
|
108
113
|
}
|
|
109
114
|
|
|
110
115
|
let content: string;
|
|
111
116
|
try {
|
|
112
117
|
content = await readFile(settingsPath, 'utf-8');
|
|
113
118
|
} catch {
|
|
114
|
-
|
|
119
|
+
throw createSettingsReadError(projectRoot);
|
|
115
120
|
}
|
|
116
121
|
|
|
117
|
-
let
|
|
122
|
+
let parsed: unknown;
|
|
118
123
|
try {
|
|
119
|
-
|
|
124
|
+
parsed = JSON.parse(content);
|
|
120
125
|
} catch {
|
|
121
|
-
|
|
126
|
+
throw createSettingsReadError(projectRoot);
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
|
|
129
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
130
|
+
throw createSettingsReadError(projectRoot);
|
|
131
|
+
}
|
|
132
|
+
const settings = parsed as UnityMcpSettings;
|
|
133
|
+
|
|
134
|
+
// Only block when isServerRunning is explicitly false (Unity clean shutdown).
|
|
135
|
+
// undefined/missing means old settings format — proceed to next validation stage.
|
|
136
|
+
if (settings.isServerRunning === false) {
|
|
137
|
+
throw new UnityNotRunningError(projectRoot);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const port = resolvePortFromUnitySettings(settings);
|
|
141
|
+
if (port === null) {
|
|
142
|
+
throw createSettingsReadError(projectRoot);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return port;
|
|
125
146
|
}
|
package/src/project-root.ts
CHANGED
|
@@ -143,7 +143,7 @@ export function findUnityProjectRoot(startPath: string = process.cwd()): string
|
|
|
143
143
|
return findUnityProjectInParents(startPath);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
interface UnityProjectStatus {
|
|
147
147
|
found: boolean;
|
|
148
148
|
path: string | null;
|
|
149
149
|
hasUloop: boolean;
|