uloop-cli 0.65.0 → 0.66.1
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/dist/cli.bundle.cjs +103 -41
- package/dist/cli.bundle.cjs.map +3 -3
- package/package.json +8 -5
- package/src/__tests__/port-resolver.test.ts +31 -1
- package/src/cli.ts +35 -9
- package/src/commands/focus-window.ts +20 -2
- package/src/default-tools.json +1 -1
- package/src/execute-tool.ts +15 -10
- package/src/port-resolver.ts +42 -4
- package/src/project-root.ts +2 -2
- package/src/skills/skill-definitions/cli-only/uloop-focus-window/SKILL.md +7 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uloop-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.66.1",
|
|
4
4
|
"//version": "x-release-please-version",
|
|
5
5
|
"description": "CLI tool for Unity Editor communication via uLoopMCP",
|
|
6
6
|
"main": "dist/cli.bundle.cjs",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"homepage": "https://github.com/hatayama/uLoopMCP#readme",
|
|
36
36
|
"engines": {
|
|
37
|
-
"node": ">=20.
|
|
37
|
+
"node": ">=20.19.0"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public",
|
|
@@ -46,20 +46,23 @@
|
|
|
46
46
|
"semver": "7.7.4"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@eslint/js": "
|
|
49
|
+
"@eslint/js": "10.0.1",
|
|
50
50
|
"@types/jest": "30.0.0",
|
|
51
51
|
"@types/node": "25.3.0",
|
|
52
52
|
"@types/semver": "7.7.1",
|
|
53
53
|
"esbuild": "0.27.3",
|
|
54
|
-
"eslint": "
|
|
54
|
+
"eslint": "10.0.1",
|
|
55
55
|
"eslint-config-prettier": "10.1.8",
|
|
56
56
|
"eslint-plugin-prettier": "5.5.5",
|
|
57
|
-
"eslint-plugin-security": "
|
|
57
|
+
"eslint-plugin-security": "4.0.0",
|
|
58
58
|
"jest": "30.2.0",
|
|
59
59
|
"prettier": "3.8.1",
|
|
60
60
|
"ts-jest": "29.4.6",
|
|
61
61
|
"tsx": "4.21.0",
|
|
62
62
|
"typescript": "5.9.3",
|
|
63
63
|
"typescript-eslint": "8.56.0"
|
|
64
|
+
},
|
|
65
|
+
"overrides": {
|
|
66
|
+
"minimatch": "10.2.2"
|
|
64
67
|
}
|
|
65
68
|
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { tmpdir } from 'os';
|
|
2
|
+
import {
|
|
3
|
+
resolvePortFromUnitySettings,
|
|
4
|
+
validateProjectPath,
|
|
5
|
+
resolveUnityPort,
|
|
6
|
+
} from '../port-resolver.js';
|
|
2
7
|
|
|
3
8
|
describe('resolvePortFromUnitySettings', () => {
|
|
4
9
|
it('returns serverPort when server is running and serverPort is valid', () => {
|
|
@@ -61,3 +66,28 @@ describe('resolvePortFromUnitySettings', () => {
|
|
|
61
66
|
expect(port).toBeNull();
|
|
62
67
|
});
|
|
63
68
|
});
|
|
69
|
+
|
|
70
|
+
describe('validateProjectPath', () => {
|
|
71
|
+
it('throws when path does not exist', () => {
|
|
72
|
+
expect(() => validateProjectPath('/nonexistent/path/to/project')).toThrow(
|
|
73
|
+
'Path does not exist: /nonexistent/path/to/project',
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('throws when path is not a Unity project', () => {
|
|
78
|
+
expect(() => validateProjectPath(tmpdir())).toThrow('Not a Unity project');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('resolveUnityPort', () => {
|
|
83
|
+
it('throws when both port and projectPath are specified', async () => {
|
|
84
|
+
await expect(resolveUnityPort(8700, '/some/path')).rejects.toThrow(
|
|
85
|
+
'Cannot specify both --port and --project-path',
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('returns explicit port when only port is specified', async () => {
|
|
90
|
+
const port = await resolveUnityPort(8711);
|
|
91
|
+
expect(port).toBe(8711);
|
|
92
|
+
});
|
|
93
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { registerLaunchCommand } from './commands/launch.js';
|
|
|
33
33
|
import { registerFocusWindowCommand } from './commands/focus-window.js';
|
|
34
34
|
import { VERSION } from './version.js';
|
|
35
35
|
import { findUnityProjectRoot } from './project-root.js';
|
|
36
|
+
import { validateProjectPath } from './port-resolver.js';
|
|
36
37
|
|
|
37
38
|
interface CliOptions extends GlobalOptions {
|
|
38
39
|
[key: string]: unknown;
|
|
@@ -73,16 +74,18 @@ program
|
|
|
73
74
|
.command('list')
|
|
74
75
|
.description('List all available tools from Unity')
|
|
75
76
|
.option('-p, --port <port>', 'Unity TCP port')
|
|
77
|
+
.option('--project-path <path>', 'Unity project path')
|
|
76
78
|
.action(async (options: CliOptions) => {
|
|
77
|
-
await runWithErrorHandling(() => listAvailableTools(options));
|
|
79
|
+
await runWithErrorHandling(() => listAvailableTools(extractGlobalOptions(options)));
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
program
|
|
81
83
|
.command('sync')
|
|
82
84
|
.description('Sync tool definitions from Unity to local cache')
|
|
83
85
|
.option('-p, --port <port>', 'Unity TCP port')
|
|
86
|
+
.option('--project-path <path>', 'Unity project path')
|
|
84
87
|
.action(async (options: CliOptions) => {
|
|
85
|
-
await runWithErrorHandling(() => syncTools(options));
|
|
88
|
+
await runWithErrorHandling(() => syncTools(extractGlobalOptions(options)));
|
|
86
89
|
});
|
|
87
90
|
|
|
88
91
|
program
|
|
@@ -104,8 +107,12 @@ program
|
|
|
104
107
|
program
|
|
105
108
|
.command('fix')
|
|
106
109
|
.description('Clean up stale lock files that may prevent CLI from connecting')
|
|
107
|
-
.
|
|
108
|
-
|
|
110
|
+
.option('--project-path <path>', 'Unity project path')
|
|
111
|
+
.action(async (options: { projectPath?: string }) => {
|
|
112
|
+
await runWithErrorHandling(() => {
|
|
113
|
+
cleanupLockFiles(options.projectPath);
|
|
114
|
+
return Promise.resolve();
|
|
115
|
+
});
|
|
109
116
|
});
|
|
110
117
|
|
|
111
118
|
// Register skills subcommand
|
|
@@ -144,6 +151,7 @@ function registerToolCommand(tool: ToolDefinition): void {
|
|
|
144
151
|
|
|
145
152
|
// Add global options
|
|
146
153
|
cmd.option('-p, --port <port>', 'Unity TCP port');
|
|
154
|
+
cmd.option('--project-path <path>', 'Unity project path');
|
|
147
155
|
|
|
148
156
|
cmd.action(async (options: CliOptions) => {
|
|
149
157
|
const params = buildParams(options, properties);
|
|
@@ -294,6 +302,7 @@ function convertValue(value: unknown, propInfo: ToolProperty): unknown {
|
|
|
294
302
|
function extractGlobalOptions(options: Record<string, unknown>): GlobalOptions {
|
|
295
303
|
return {
|
|
296
304
|
port: options['port'] as string | undefined,
|
|
305
|
+
projectPath: options['projectPath'] as string | undefined,
|
|
297
306
|
};
|
|
298
307
|
}
|
|
299
308
|
|
|
@@ -592,8 +601,9 @@ const LOCK_FILES = ['compiling.lock', 'domainreload.lock', 'serverstarting.lock'
|
|
|
592
601
|
/**
|
|
593
602
|
* Clean up stale lock files that may prevent CLI from connecting to Unity.
|
|
594
603
|
*/
|
|
595
|
-
function cleanupLockFiles(): void {
|
|
596
|
-
const projectRoot =
|
|
604
|
+
function cleanupLockFiles(projectPath?: string): void {
|
|
605
|
+
const projectRoot =
|
|
606
|
+
projectPath !== undefined ? validateProjectPath(projectPath) : findUnityProjectRoot();
|
|
597
607
|
if (projectRoot === null) {
|
|
598
608
|
console.error('Could not find Unity project root.');
|
|
599
609
|
process.exit(1);
|
|
@@ -757,22 +767,38 @@ function shouldSkipAutoSync(cmdName: string | undefined, args: string[]): boolea
|
|
|
757
767
|
}
|
|
758
768
|
|
|
759
769
|
function extractSyncGlobalOptions(args: string[]): GlobalOptions {
|
|
770
|
+
const options: GlobalOptions = {};
|
|
771
|
+
|
|
760
772
|
for (let i = 0; i < args.length; i++) {
|
|
761
773
|
const arg = args[i];
|
|
762
774
|
if (arg === '--port' || arg === '-p') {
|
|
763
775
|
const nextArg = args[i + 1];
|
|
764
776
|
if (nextArg !== undefined && !nextArg.startsWith('-')) {
|
|
765
|
-
|
|
777
|
+
options.port = nextArg;
|
|
766
778
|
}
|
|
767
779
|
continue;
|
|
768
780
|
}
|
|
769
781
|
|
|
770
782
|
if (arg.startsWith('--port=')) {
|
|
771
|
-
|
|
783
|
+
options.port = arg.slice('--port='.length);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (arg === '--project-path') {
|
|
788
|
+
const nextArg = args[i + 1];
|
|
789
|
+
if (nextArg !== undefined && !nextArg.startsWith('-')) {
|
|
790
|
+
options.projectPath = nextArg;
|
|
791
|
+
}
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (arg.startsWith('--project-path=')) {
|
|
796
|
+
options.projectPath = arg.slice('--project-path='.length);
|
|
797
|
+
continue;
|
|
772
798
|
}
|
|
773
799
|
}
|
|
774
800
|
|
|
775
|
-
return
|
|
801
|
+
return options;
|
|
776
802
|
}
|
|
777
803
|
|
|
778
804
|
/**
|
|
@@ -10,13 +10,31 @@
|
|
|
10
10
|
import { Command } from 'commander';
|
|
11
11
|
import { findRunningUnityProcess, focusUnityProcess } from 'launch-unity';
|
|
12
12
|
import { findUnityProjectRoot } from '../project-root.js';
|
|
13
|
+
import { validateProjectPath } from '../port-resolver.js';
|
|
13
14
|
|
|
14
15
|
export function registerFocusWindowCommand(program: Command): void {
|
|
15
16
|
program
|
|
16
17
|
.command('focus-window')
|
|
17
18
|
.description('Bring Unity Editor window to front using OS-level commands')
|
|
18
|
-
.
|
|
19
|
-
|
|
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
|
+
}
|
|
20
38
|
if (projectRoot === null) {
|
|
21
39
|
console.error(
|
|
22
40
|
JSON.stringify({
|
package/src/default-tools.json
CHANGED
package/src/execute-tool.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { existsSync } from 'fs';
|
|
|
12
12
|
import { join } from 'path';
|
|
13
13
|
import * as semver from 'semver';
|
|
14
14
|
import { DirectUnityClient } from './direct-unity-client.js';
|
|
15
|
-
import { resolveUnityPort } from './port-resolver.js';
|
|
15
|
+
import { resolveUnityPort, validateProjectPath } from './port-resolver.js';
|
|
16
16
|
import { saveToolsCache, getCacheFilePath, ToolsCache, ToolDefinition } from './tool-cache.js';
|
|
17
17
|
import { VERSION } from './version.js';
|
|
18
18
|
import { createSpinner } from './spinner.js';
|
|
@@ -61,6 +61,7 @@ function suppressStdinEcho(): () => void {
|
|
|
61
61
|
|
|
62
62
|
export interface GlobalOptions {
|
|
63
63
|
port?: string;
|
|
64
|
+
projectPath?: string;
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
function stripInternalFields(result: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -162,8 +163,9 @@ function checkServerVersion(result: Record<string, unknown>): void {
|
|
|
162
163
|
* Check if Unity is in a busy state (compiling, reloading, or server starting).
|
|
163
164
|
* Throws an error with appropriate message if busy.
|
|
164
165
|
*/
|
|
165
|
-
function checkUnityBusyState(): void {
|
|
166
|
-
const projectRoot =
|
|
166
|
+
function checkUnityBusyState(projectPath?: string): void {
|
|
167
|
+
const projectRoot =
|
|
168
|
+
projectPath !== undefined ? validateProjectPath(projectPath) : findUnityProjectRoot();
|
|
167
169
|
if (projectRoot === null) {
|
|
168
170
|
return;
|
|
169
171
|
}
|
|
@@ -197,7 +199,7 @@ export async function executeToolCommand(
|
|
|
197
199
|
}
|
|
198
200
|
portNumber = parsed;
|
|
199
201
|
}
|
|
200
|
-
const port = await resolveUnityPort(portNumber);
|
|
202
|
+
const port = await resolveUnityPort(portNumber, globalOptions.projectPath);
|
|
201
203
|
const compileOptions = getCompileExecutionOptions(toolName, params);
|
|
202
204
|
const shouldWaitForDomainReload = compileOptions.waitForDomainReload;
|
|
203
205
|
const compileRequestId = shouldWaitForDomainReload ? ensureCompileRequestId(params) : undefined;
|
|
@@ -207,7 +209,10 @@ export async function executeToolCommand(
|
|
|
207
209
|
|
|
208
210
|
let lastError: unknown;
|
|
209
211
|
let immediateResult: Record<string, unknown> | undefined;
|
|
210
|
-
const projectRoot =
|
|
212
|
+
const projectRoot =
|
|
213
|
+
globalOptions.projectPath !== undefined
|
|
214
|
+
? validateProjectPath(globalOptions.projectPath)
|
|
215
|
+
: findUnityProjectRoot();
|
|
211
216
|
|
|
212
217
|
// Monotonically-increasing flag: once true, retries cannot reset it to false.
|
|
213
218
|
// The retry loop overwrites `lastError` and `immediateResult` on each attempt,
|
|
@@ -218,7 +223,7 @@ export async function executeToolCommand(
|
|
|
218
223
|
let requestDispatched = false;
|
|
219
224
|
|
|
220
225
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
221
|
-
checkUnityBusyState();
|
|
226
|
+
checkUnityBusyState(globalOptions.projectPath);
|
|
222
227
|
|
|
223
228
|
const client = new DirectUnityClient(port);
|
|
224
229
|
try {
|
|
@@ -377,14 +382,14 @@ export async function listAvailableTools(globalOptions: GlobalOptions): Promise<
|
|
|
377
382
|
}
|
|
378
383
|
portNumber = parsed;
|
|
379
384
|
}
|
|
380
|
-
const port = await resolveUnityPort(portNumber);
|
|
385
|
+
const port = await resolveUnityPort(portNumber, globalOptions.projectPath);
|
|
381
386
|
|
|
382
387
|
const restoreStdin = suppressStdinEcho();
|
|
383
388
|
const spinner = createSpinner('Connecting to Unity...');
|
|
384
389
|
|
|
385
390
|
let lastError: unknown;
|
|
386
391
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
387
|
-
checkUnityBusyState();
|
|
392
|
+
checkUnityBusyState(globalOptions.projectPath);
|
|
388
393
|
|
|
389
394
|
const client = new DirectUnityClient(port);
|
|
390
395
|
try {
|
|
@@ -465,14 +470,14 @@ export async function syncTools(globalOptions: GlobalOptions): Promise<void> {
|
|
|
465
470
|
}
|
|
466
471
|
portNumber = parsed;
|
|
467
472
|
}
|
|
468
|
-
const port = await resolveUnityPort(portNumber);
|
|
473
|
+
const port = await resolveUnityPort(portNumber, globalOptions.projectPath);
|
|
469
474
|
|
|
470
475
|
const restoreStdin = suppressStdinEcho();
|
|
471
476
|
const spinner = createSpinner('Connecting to Unity...');
|
|
472
477
|
|
|
473
478
|
let lastError: unknown;
|
|
474
479
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
475
|
-
checkUnityBusyState();
|
|
480
|
+
checkUnityBusyState(globalOptions.projectPath);
|
|
476
481
|
|
|
477
482
|
const client = new DirectUnityClient(port);
|
|
478
483
|
try {
|
package/src/port-resolver.ts
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
import { readFile } from 'fs/promises';
|
|
10
10
|
import { existsSync } from 'fs';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
import { findUnityProjectRoot } from './project-root.js';
|
|
11
|
+
import { join, resolve } from 'path';
|
|
12
|
+
import { findUnityProjectRoot, isUnityProject, hasUloopInstalled } from './project-root.js';
|
|
13
13
|
|
|
14
14
|
const DEFAULT_PORT = 8700;
|
|
15
15
|
|
|
@@ -54,14 +54,52 @@ export function resolvePortFromUnitySettings(settings: UnityMcpSettings): number
|
|
|
54
54
|
return null;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export
|
|
57
|
+
export function validateProjectPath(projectPath: string): string {
|
|
58
|
+
const resolved = resolve(projectPath);
|
|
59
|
+
|
|
60
|
+
if (!existsSync(resolved)) {
|
|
61
|
+
throw new Error(`Path does not exist: ${resolved}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!isUnityProject(resolved)) {
|
|
65
|
+
throw new Error(`Not a Unity project (Assets/ or ProjectSettings/ not found): ${resolved}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!hasUloopInstalled(resolved)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`uLoopMCP is not installed in this project (UserSettings/UnityMcpSettings.json not found): ${resolved}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return resolved;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function resolveUnityPort(
|
|
78
|
+
explicitPort?: number,
|
|
79
|
+
projectPath?: string,
|
|
80
|
+
): Promise<number> {
|
|
81
|
+
if (explicitPort !== undefined && projectPath !== undefined) {
|
|
82
|
+
throw new Error('Cannot specify both --port and --project-path. Use one or the other.');
|
|
83
|
+
}
|
|
84
|
+
|
|
58
85
|
if (explicitPort !== undefined) {
|
|
59
86
|
return explicitPort;
|
|
60
87
|
}
|
|
61
88
|
|
|
89
|
+
if (projectPath !== undefined) {
|
|
90
|
+
const resolved = validateProjectPath(projectPath);
|
|
91
|
+
const settingsPort = await readPortFromSettings(resolved);
|
|
92
|
+
if (settingsPort !== null) {
|
|
93
|
+
return settingsPort;
|
|
94
|
+
}
|
|
95
|
+
return DEFAULT_PORT;
|
|
96
|
+
}
|
|
97
|
+
|
|
62
98
|
const projectRoot = findUnityProjectRoot();
|
|
63
99
|
if (projectRoot === null) {
|
|
64
|
-
throw new Error(
|
|
100
|
+
throw new Error(
|
|
101
|
+
'Unity project not found. Use --port or --project-path option to specify the target.',
|
|
102
|
+
);
|
|
65
103
|
}
|
|
66
104
|
|
|
67
105
|
const settingsPort = await readPortFromSettings(projectRoot);
|
package/src/project-root.ts
CHANGED
|
@@ -22,13 +22,13 @@ const EXCLUDED_DIRS = new Set([
|
|
|
22
22
|
'Library',
|
|
23
23
|
]);
|
|
24
24
|
|
|
25
|
-
function isUnityProject(dirPath: string): boolean {
|
|
25
|
+
export function isUnityProject(dirPath: string): boolean {
|
|
26
26
|
const hasAssets = existsSync(join(dirPath, 'Assets'));
|
|
27
27
|
const hasProjectSettings = existsSync(join(dirPath, 'ProjectSettings'));
|
|
28
28
|
return hasAssets && hasProjectSettings;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
function hasUloopInstalled(dirPath: string): boolean {
|
|
31
|
+
export function hasUloopInstalled(dirPath: string): boolean {
|
|
32
32
|
return existsSync(join(dirPath, 'UserSettings/UnityMcpSettings.json'));
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -17,6 +17,13 @@ uloop focus-window
|
|
|
17
17
|
|
|
18
18
|
None.
|
|
19
19
|
|
|
20
|
+
## Global Options
|
|
21
|
+
|
|
22
|
+
| Option | Description |
|
|
23
|
+
|--------|-------------|
|
|
24
|
+
| `--project-path <path>` | Target a specific Unity project (mutually exclusive with `--port`). Path resolution follows the same rules as `cd` — absolute paths are used as-is, relative paths are resolved from cwd. |
|
|
25
|
+
| `-p, --port <port>` | Specify Unity TCP port directly (mutually exclusive with `--project-path`). |
|
|
26
|
+
|
|
20
27
|
## Examples
|
|
21
28
|
|
|
22
29
|
```bash
|
package/src/version.ts
CHANGED
|
@@ -4,4 +4,4 @@
|
|
|
4
4
|
* This file exists to avoid bundling the entire package.json into the CLI bundle.
|
|
5
5
|
* This version is automatically updated by release-please.
|
|
6
6
|
*/
|
|
7
|
-
export const VERSION = '0.
|
|
7
|
+
export const VERSION = '0.66.1'; // x-release-please-version
|