uloop-cli 0.44.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/.prettierrc.json +28 -0
- package/CLAUDE.md +61 -0
- package/dist/cli.bundle.cjs +4761 -0
- package/dist/cli.bundle.cjs.map +7 -0
- package/eslint.config.mjs +72 -0
- package/jest.config.cjs +19 -0
- package/package.json +61 -0
- package/src/__tests__/cli-e2e.test.ts +349 -0
- package/src/__tests__/setup.ts +24 -0
- package/src/arg-parser.ts +128 -0
- package/src/cli.ts +489 -0
- package/src/default-tools.json +355 -0
- package/src/direct-unity-client.ts +125 -0
- package/src/execute-tool.ts +155 -0
- package/src/port-resolver.ts +60 -0
- package/src/project-root.ts +31 -0
- package/src/simple-framer.ts +97 -0
- package/src/skills/bundled-skills.ts +64 -0
- package/src/skills/markdown.d.ts +4 -0
- package/src/skills/skill-definitions/uloop-capture-gameview/SKILL.md +39 -0
- package/src/skills/skill-definitions/uloop-clear-console/SKILL.md +34 -0
- package/src/skills/skill-definitions/uloop-compile/SKILL.md +37 -0
- package/src/skills/skill-definitions/uloop-execute-dynamic-code/SKILL.md +79 -0
- package/src/skills/skill-definitions/uloop-execute-menu-item/SKILL.md +43 -0
- package/src/skills/skill-definitions/uloop-find-game-objects/SKILL.md +46 -0
- package/src/skills/skill-definitions/uloop-focus-window/SKILL.md +34 -0
- package/src/skills/skill-definitions/uloop-get-hierarchy/SKILL.md +44 -0
- package/src/skills/skill-definitions/uloop-get-logs/SKILL.md +45 -0
- package/src/skills/skill-definitions/uloop-get-menu-items/SKILL.md +44 -0
- package/src/skills/skill-definitions/uloop-get-project-info/SKILL.md +34 -0
- package/src/skills/skill-definitions/uloop-get-provider-details/SKILL.md +45 -0
- package/src/skills/skill-definitions/uloop-get-version/SKILL.md +31 -0
- package/src/skills/skill-definitions/uloop-run-tests/SKILL.md +43 -0
- package/src/skills/skill-definitions/uloop-unity-search/SKILL.md +44 -0
- package/src/skills/skills-command.ts +118 -0
- package/src/skills/skills-manager.ts +135 -0
- package/src/tool-cache.ts +104 -0
- package/src/version.ts +7 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import tseslint from 'typescript-eslint';
|
|
3
|
+
import eslintPluginPrettier from 'eslint-plugin-prettier/recommended';
|
|
4
|
+
import eslintPluginSecurity from 'eslint-plugin-security';
|
|
5
|
+
|
|
6
|
+
export default tseslint.config(
|
|
7
|
+
// Global ignores
|
|
8
|
+
{
|
|
9
|
+
ignores: ['dist/', 'node_modules/', '**/*.js', '**/*.d.ts'],
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
// Base ESLint recommended rules
|
|
13
|
+
eslint.configs.recommended,
|
|
14
|
+
|
|
15
|
+
// TypeScript ESLint recommended rules
|
|
16
|
+
...tseslint.configs.recommendedTypeChecked,
|
|
17
|
+
|
|
18
|
+
// Prettier integration
|
|
19
|
+
eslintPluginPrettier,
|
|
20
|
+
|
|
21
|
+
// Security plugin
|
|
22
|
+
{
|
|
23
|
+
plugins: {
|
|
24
|
+
security: eslintPluginSecurity,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
// Main configuration for TypeScript files
|
|
29
|
+
{
|
|
30
|
+
files: ['src/**/*.ts'],
|
|
31
|
+
languageOptions: {
|
|
32
|
+
parserOptions: {
|
|
33
|
+
projectService: true,
|
|
34
|
+
tsconfigRootDir: import.meta.dirname,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
rules: {
|
|
38
|
+
// Prettier
|
|
39
|
+
'prettier/prettier': 'error',
|
|
40
|
+
|
|
41
|
+
// TypeScript rules
|
|
42
|
+
'@typescript-eslint/explicit-function-return-type': 'error',
|
|
43
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
44
|
+
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
|
45
|
+
'@typescript-eslint/no-non-null-assertion': 'warn',
|
|
46
|
+
|
|
47
|
+
// General rules
|
|
48
|
+
'no-console': 'warn',
|
|
49
|
+
'no-debugger': 'error',
|
|
50
|
+
'no-var': 'error',
|
|
51
|
+
'prefer-const': 'error',
|
|
52
|
+
eqeqeq: ['error', 'always'],
|
|
53
|
+
curly: ['error', 'all'],
|
|
54
|
+
'no-duplicate-imports': 'error',
|
|
55
|
+
|
|
56
|
+
// Security rules
|
|
57
|
+
'security/detect-eval-with-expression': 'error',
|
|
58
|
+
'security/detect-non-literal-fs-filename': 'warn',
|
|
59
|
+
'security/detect-non-literal-regexp': 'warn',
|
|
60
|
+
'security/detect-non-literal-require': 'error',
|
|
61
|
+
'security/detect-object-injection': 'warn',
|
|
62
|
+
'security/detect-possible-timing-attacks': 'warn',
|
|
63
|
+
'security/detect-pseudoRandomBytes': 'warn',
|
|
64
|
+
'security/detect-unsafe-regex': 'error',
|
|
65
|
+
'security/detect-buffer-noassert': 'error',
|
|
66
|
+
'security/detect-child-process': 'warn',
|
|
67
|
+
'security/detect-disable-mustache-escape': 'error',
|
|
68
|
+
'security/detect-new-buffer': 'error',
|
|
69
|
+
'security/detect-no-csrf-before-method-override': 'error',
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
);
|
package/jest.config.cjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** @type {import('jest').Config} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
preset: 'ts-jest',
|
|
4
|
+
testEnvironment: 'node',
|
|
5
|
+
moduleFileExtensions: ['ts', 'js', 'json'],
|
|
6
|
+
testMatch: ['**/__tests__/**/*.test.ts'],
|
|
7
|
+
transform: {
|
|
8
|
+
'^.+\\.ts$': 'ts-jest',
|
|
9
|
+
},
|
|
10
|
+
moduleNameMapper: {
|
|
11
|
+
'^(\\.{1,2}/.*)\\.js$': '$1',
|
|
12
|
+
},
|
|
13
|
+
collectCoverageFrom: [
|
|
14
|
+
'src/**/*.ts',
|
|
15
|
+
'!src/**/*.d.ts',
|
|
16
|
+
'!src/**/__tests__/**',
|
|
17
|
+
],
|
|
18
|
+
setupFilesAfterEnv: ['<rootDir>/src/__tests__/setup.ts'],
|
|
19
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "uloop-cli",
|
|
3
|
+
"version": "0.44.1",
|
|
4
|
+
"//version": "x-release-please-version",
|
|
5
|
+
"description": "CLI tool for Unity Editor communication via uLoopMCP",
|
|
6
|
+
"main": "dist/cli.bundle.cjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"uloop": "./dist/cli.bundle.cjs"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "esbuild src/cli.ts --bundle --platform=node --format=cjs --outfile=dist/cli.bundle.cjs --sourcemap --banner:js='#!/usr/bin/env node' --loader:.md=text",
|
|
13
|
+
"lint": "eslint src",
|
|
14
|
+
"lint:fix": "eslint src --fix",
|
|
15
|
+
"format": "prettier --write src/**/*.ts",
|
|
16
|
+
"format:check": "prettier --check src/**/*.ts",
|
|
17
|
+
"test:cli": "jest src/__tests__ --testTimeout=60000 --runInBand"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"cli",
|
|
21
|
+
"unity",
|
|
22
|
+
"mcp",
|
|
23
|
+
"uloop"
|
|
24
|
+
],
|
|
25
|
+
"author": "hatayama",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/hatayama/uLoopMCP.git",
|
|
30
|
+
"directory": "Packages/src/Cli~"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/hatayama/uLoopMCP/issues"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/hatayama/uLoopMCP#readme",
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20.0.0"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public",
|
|
41
|
+
"provenance": true
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"commander": "^14.0.2"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@eslint/js": "9.39.2",
|
|
48
|
+
"@types/jest": "30.0.0",
|
|
49
|
+
"@types/node": "25.0.2",
|
|
50
|
+
"esbuild": "0.27.1",
|
|
51
|
+
"eslint": "9.39.2",
|
|
52
|
+
"eslint-config-prettier": "^10.1.8",
|
|
53
|
+
"eslint-plugin-prettier": "5.5.4",
|
|
54
|
+
"eslint-plugin-security": "3.0.1",
|
|
55
|
+
"jest": "30.2.0",
|
|
56
|
+
"prettier": "3.7.4",
|
|
57
|
+
"ts-jest": "29.4.6",
|
|
58
|
+
"typescript": "5.9.3",
|
|
59
|
+
"typescript-eslint": "8.49.0"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI End-to-End Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests require a running Unity Editor with uLoopMCP installed.
|
|
5
|
+
* Run from Unity project root: npm run test:cli
|
|
6
|
+
*
|
|
7
|
+
* @jest-environment node
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { execSync, ExecSyncOptionsWithStringEncoding } from 'child_process';
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
|
|
13
|
+
const CLI_PATH = join(__dirname, '../..', 'dist/cli.bundle.cjs');
|
|
14
|
+
|
|
15
|
+
const UNITY_PROJECT_ROOT = join(__dirname, '../../../../..');
|
|
16
|
+
|
|
17
|
+
const EXEC_OPTIONS: ExecSyncOptionsWithStringEncoding = {
|
|
18
|
+
encoding: 'utf-8',
|
|
19
|
+
timeout: 60000,
|
|
20
|
+
cwd: UNITY_PROJECT_ROOT,
|
|
21
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const INTERVAL_MS = 1500;
|
|
25
|
+
const DOMAIN_RELOAD_RETRY_MS = 3000;
|
|
26
|
+
const DOMAIN_RELOAD_MAX_RETRIES = 3;
|
|
27
|
+
|
|
28
|
+
function sleep(ms: number): Promise<void> {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sleepSync(ms: number): void {
|
|
33
|
+
const end = Date.now() + ms;
|
|
34
|
+
while (Date.now() < end) {
|
|
35
|
+
// busy wait
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isDomainReloadError(output: string): boolean {
|
|
40
|
+
return output.includes('Unity is reloading') || output.includes('Domain Reload');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function runCli(args: string): { stdout: string; stderr: string; exitCode: number } {
|
|
44
|
+
try {
|
|
45
|
+
const stdout = execSync(`node "${CLI_PATH}" ${args}`, EXEC_OPTIONS);
|
|
46
|
+
return { stdout, stderr: '', exitCode: 0 };
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const execError = error as { stdout?: string; stderr?: string; status?: number };
|
|
49
|
+
return {
|
|
50
|
+
stdout: execError.stdout ?? '',
|
|
51
|
+
stderr: execError.stderr ?? '',
|
|
52
|
+
exitCode: execError.status ?? 1,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function runCliWithRetry(args: string): { stdout: string; stderr: string; exitCode: number } {
|
|
58
|
+
for (let attempt = 0; attempt < DOMAIN_RELOAD_MAX_RETRIES; attempt++) {
|
|
59
|
+
const result = runCli(args);
|
|
60
|
+
const output = result.stderr || result.stdout;
|
|
61
|
+
|
|
62
|
+
if (result.exitCode === 0 || !isDomainReloadError(output)) {
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Domain Reload in progress, wait and retry
|
|
67
|
+
if (attempt < DOMAIN_RELOAD_MAX_RETRIES - 1) {
|
|
68
|
+
sleepSync(DOMAIN_RELOAD_RETRY_MS);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return runCli(args);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function runCliJson<T>(args: string): T {
|
|
76
|
+
const { stdout, stderr, exitCode } = runCliWithRetry(args);
|
|
77
|
+
if (exitCode !== 0) {
|
|
78
|
+
throw new Error(`CLI failed with exit code ${exitCode}: ${stderr || stdout}`);
|
|
79
|
+
}
|
|
80
|
+
return JSON.parse(stdout) as T;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('CLI E2E Tests (requires running Unity)', () => {
|
|
84
|
+
beforeEach(async () => {
|
|
85
|
+
await sleep(INTERVAL_MS);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('compile', () => {
|
|
89
|
+
it('should compile successfully', () => {
|
|
90
|
+
const result = runCliJson<{ Success: boolean; ErrorCount: number }>('compile');
|
|
91
|
+
|
|
92
|
+
expect(result.Success).toBe(true);
|
|
93
|
+
expect(result.ErrorCount).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should support --force-recompile option', () => {
|
|
97
|
+
const { exitCode } = runCli('compile --force-recompile');
|
|
98
|
+
|
|
99
|
+
// Domain Reload causes connection to be lost, so we just verify the command runs
|
|
100
|
+
// The exit code may be non-zero due to connection being dropped during reload
|
|
101
|
+
expect(typeof exitCode).toBe('number');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('get-logs', () => {
|
|
106
|
+
const TEST_LOG_MENU_PATH = 'uLoopMCP/Debug/LogGetter Tests/Output Test Logs';
|
|
107
|
+
|
|
108
|
+
function setupTestLogs(): void {
|
|
109
|
+
runCli('clear-console');
|
|
110
|
+
runCli(`execute-menu-item --menu-item-path "${TEST_LOG_MENU_PATH}"`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
it('should retrieve test logs after executing Output Test Logs menu item', () => {
|
|
114
|
+
setupTestLogs();
|
|
115
|
+
|
|
116
|
+
const result = runCliJson<{ TotalCount: number; Logs: Array<{ Message: string }> }>(
|
|
117
|
+
'get-logs',
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(result.TotalCount).toBeGreaterThan(0);
|
|
121
|
+
expect(Array.isArray(result.Logs)).toBe(true);
|
|
122
|
+
|
|
123
|
+
// Verify specific test log messages exist
|
|
124
|
+
const messages = result.Logs.map((log) => log.Message);
|
|
125
|
+
expect(messages.some((m) => m.includes('This is a normal log'))).toBe(true);
|
|
126
|
+
expect(messages.some((m) => m.includes('LogGetter test complete'))).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should respect --max-count option', () => {
|
|
130
|
+
setupTestLogs();
|
|
131
|
+
|
|
132
|
+
const result = runCliJson<{ Logs: unknown[] }>('get-logs --max-count 3');
|
|
133
|
+
|
|
134
|
+
expect(result.Logs.length).toBeLessThanOrEqual(3);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should filter by log type Warning', () => {
|
|
138
|
+
setupTestLogs();
|
|
139
|
+
|
|
140
|
+
const result = runCliJson<{ Logs: Array<{ Type: string; Message: string }> }>(
|
|
141
|
+
'get-logs --log-type Warning',
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
expect(result.Logs.length).toBeGreaterThan(0);
|
|
145
|
+
for (const log of result.Logs) {
|
|
146
|
+
expect(log.Type).toBe('Warning');
|
|
147
|
+
}
|
|
148
|
+
// Verify test warning log exists
|
|
149
|
+
const messages = result.Logs.map((log) => log.Message);
|
|
150
|
+
expect(messages.some((m) => m.includes('This is a warning log'))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should filter by log type Error', () => {
|
|
154
|
+
setupTestLogs();
|
|
155
|
+
|
|
156
|
+
const result = runCliJson<{ Logs: Array<{ Type: string; Message: string }> }>(
|
|
157
|
+
'get-logs --log-type Error',
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(result.Logs.length).toBeGreaterThan(0);
|
|
161
|
+
for (const log of result.Logs) {
|
|
162
|
+
expect(log.Type).toBe('Error');
|
|
163
|
+
}
|
|
164
|
+
// Verify test error log exists
|
|
165
|
+
const messages = result.Logs.map((log) => log.Message);
|
|
166
|
+
expect(messages.some((m) => m.includes('This is an error log'))).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should search logs by text', () => {
|
|
170
|
+
setupTestLogs();
|
|
171
|
+
|
|
172
|
+
const result = runCliJson<{ Logs: Array<{ Message: string }> }>(
|
|
173
|
+
'get-logs --search-text "LogGetter test complete"',
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
expect(result.Logs.length).toBeGreaterThan(0);
|
|
177
|
+
for (const log of result.Logs) {
|
|
178
|
+
expect(log.Message).toContain('LogGetter test complete');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('clear-console', () => {
|
|
184
|
+
const TEST_LOG_MENU_PATH = 'uLoopMCP/Debug/LogGetter Tests/Output Test Logs';
|
|
185
|
+
|
|
186
|
+
it('should clear console and verify logs are empty', () => {
|
|
187
|
+
// First output some logs
|
|
188
|
+
runCli(`execute-menu-item --menu-item-path "${TEST_LOG_MENU_PATH}"`);
|
|
189
|
+
|
|
190
|
+
// Clear console
|
|
191
|
+
const result = runCliJson<{ Success: boolean }>('clear-console');
|
|
192
|
+
expect(result.Success).toBe(true);
|
|
193
|
+
|
|
194
|
+
// Verify logs are cleared
|
|
195
|
+
const logsAfterClear = runCliJson<{ TotalCount: number; Logs: unknown[] }>('get-logs');
|
|
196
|
+
expect(logsAfterClear.TotalCount).toBe(0);
|
|
197
|
+
expect(logsAfterClear.Logs.length).toBe(0);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('focus-window', () => {
|
|
202
|
+
it('should execute focus-window command', () => {
|
|
203
|
+
// Note: Success may be false in headless/CI environments where window focus is not supported
|
|
204
|
+
const result = runCliJson<{ Success: boolean }>('focus-window');
|
|
205
|
+
|
|
206
|
+
// Just verify the command executes and returns valid JSON with Success property
|
|
207
|
+
expect(typeof result.Success).toBe('boolean');
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('get-hierarchy', () => {
|
|
212
|
+
it('should retrieve hierarchy and save to file', () => {
|
|
213
|
+
const result = runCliJson<{ hierarchyFilePath: string }>('get-hierarchy --max-depth 2');
|
|
214
|
+
|
|
215
|
+
expect(typeof result.hierarchyFilePath).toBe('string');
|
|
216
|
+
expect(result.hierarchyFilePath).toContain('hierarchy_');
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
describe('get-menu-items', () => {
|
|
221
|
+
it('should retrieve menu items', () => {
|
|
222
|
+
const result = runCliJson<{ MenuItems: unknown[]; TotalCount: number }>(
|
|
223
|
+
'get-menu-items --max-count 10',
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(typeof result.TotalCount).toBe('number');
|
|
227
|
+
expect(Array.isArray(result.MenuItems)).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should filter menu items', () => {
|
|
231
|
+
const result = runCliJson<{ MenuItems: Array<{ Path: string }> }>(
|
|
232
|
+
'get-menu-items --filter-text "GameObject"',
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
expect(result.MenuItems.length).toBeGreaterThan(0);
|
|
236
|
+
for (const item of result.MenuItems) {
|
|
237
|
+
expect(item.Path.toLowerCase()).toContain('gameobject');
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
describe('execute-menu-item', () => {
|
|
243
|
+
const TEST_LOG_MENU_PATH = 'uLoopMCP/Debug/LogGetter Tests/Output Test Logs';
|
|
244
|
+
|
|
245
|
+
it('should execute menu item and verify logs are output', () => {
|
|
246
|
+
// Clear console first
|
|
247
|
+
runCli('clear-console');
|
|
248
|
+
|
|
249
|
+
// Execute menu item
|
|
250
|
+
const result = runCliJson<{ Success: boolean; MenuItemPath: string }>(
|
|
251
|
+
`execute-menu-item --menu-item-path "${TEST_LOG_MENU_PATH}"`,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
expect(result.Success).toBe(true);
|
|
255
|
+
expect(result.MenuItemPath).toBe(TEST_LOG_MENU_PATH);
|
|
256
|
+
|
|
257
|
+
// Verify logs were output
|
|
258
|
+
const logs = runCliJson<{ TotalCount: number; Logs: Array<{ Message: string }> }>('get-logs');
|
|
259
|
+
expect(logs.TotalCount).toBeGreaterThan(0);
|
|
260
|
+
const messages = logs.Logs.map((log) => log.Message);
|
|
261
|
+
expect(messages.some((m) => m.includes('LogGetter test complete'))).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('unity-search', () => {
|
|
266
|
+
it('should search assets', () => {
|
|
267
|
+
const result = runCliJson<{ Results: unknown[] }>('unity-search --search-query "*.cs"');
|
|
268
|
+
|
|
269
|
+
expect(Array.isArray(result.Results)).toBe(true);
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe('find-game-objects', () => {
|
|
274
|
+
it('should find game objects with name pattern', () => {
|
|
275
|
+
const result = runCliJson<{ results: unknown[]; totalFound: number }>(
|
|
276
|
+
'find-game-objects --name-pattern "*" --include-inactive',
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
expect(Array.isArray(result.results)).toBe(true);
|
|
280
|
+
expect(typeof result.totalFound).toBe('number');
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('get-provider-details', () => {
|
|
285
|
+
it('should retrieve search providers', () => {
|
|
286
|
+
const result = runCliJson<{ Providers: unknown[] }>('get-provider-details');
|
|
287
|
+
|
|
288
|
+
expect(Array.isArray(result.Providers)).toBe(true);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('--help', () => {
|
|
293
|
+
it('should display help', () => {
|
|
294
|
+
const { stdout, exitCode } = runCli('--help');
|
|
295
|
+
|
|
296
|
+
expect(exitCode).toBe(0);
|
|
297
|
+
expect(stdout).toContain('Unity MCP CLI');
|
|
298
|
+
expect(stdout).toContain('compile');
|
|
299
|
+
expect(stdout).toContain('get-logs');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should display command-specific help', () => {
|
|
303
|
+
const { stdout, exitCode } = runCli('compile --help');
|
|
304
|
+
|
|
305
|
+
expect(exitCode).toBe(0);
|
|
306
|
+
expect(stdout).toContain('--force-recompile');
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
describe('--version', () => {
|
|
311
|
+
it('should display version', () => {
|
|
312
|
+
const { stdout, exitCode } = runCli('--version');
|
|
313
|
+
|
|
314
|
+
expect(exitCode).toBe(0);
|
|
315
|
+
expect(stdout).toMatch(/^\d+\.\d+\.\d+/);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe('list', () => {
|
|
320
|
+
it('should list available tools', () => {
|
|
321
|
+
const { stdout, exitCode } = runCli('list');
|
|
322
|
+
|
|
323
|
+
expect(exitCode).toBe(0);
|
|
324
|
+
expect(stdout).toContain('- compile');
|
|
325
|
+
expect(stdout).toContain('- get-logs');
|
|
326
|
+
expect(stdout).toContain('- get-hierarchy');
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
describe('sync', () => {
|
|
331
|
+
it('should sync tools from Unity', () => {
|
|
332
|
+
const { stdout, exitCode } = runCli('sync');
|
|
333
|
+
|
|
334
|
+
expect(exitCode).toBe(0);
|
|
335
|
+
expect(stdout).toContain('Synced');
|
|
336
|
+
expect(stdout).toContain('tools to');
|
|
337
|
+
// Check for tools.json in path (works for both Windows \ and Unix /)
|
|
338
|
+
expect(stdout).toMatch(/[/\\]\.uloop[/\\]tools\.json/);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
describe('error handling', () => {
|
|
343
|
+
it('should handle unknown commands gracefully', () => {
|
|
344
|
+
const { exitCode } = runCli('unknown-command');
|
|
345
|
+
|
|
346
|
+
expect(exitCode).not.toBe(0);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Jest Setup Configuration for CLI tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Suppress console noise during tests (can be overridden in individual tests)
|
|
6
|
+
// eslint-disable-next-line no-console
|
|
7
|
+
const originalConsoleWarn = console.warn;
|
|
8
|
+
// eslint-disable-next-line no-console
|
|
9
|
+
const originalConsoleError = console.error;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.clearAllMocks();
|
|
13
|
+
// eslint-disable-next-line no-console
|
|
14
|
+
console.warn = jest.fn();
|
|
15
|
+
// eslint-disable-next-line no-console
|
|
16
|
+
console.error = jest.fn();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
// eslint-disable-next-line no-console
|
|
21
|
+
console.warn = originalConsoleWarn;
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.error = originalConsoleError;
|
|
24
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI argument parser for tool parameters.
|
|
3
|
+
* Converts CLI options to Unity tool parameters.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ToolParameter {
|
|
7
|
+
Type: string;
|
|
8
|
+
Description: string;
|
|
9
|
+
DefaultValue?: unknown;
|
|
10
|
+
Enum?: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ToolSchema {
|
|
14
|
+
properties: Record<string, ToolParameter>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Converts kebab-case CLI option name to PascalCase parameter name.
|
|
19
|
+
* e.g., "force-recompile" -> "ForceRecompile"
|
|
20
|
+
*/
|
|
21
|
+
export function kebabToPascalCase(kebab: string): string {
|
|
22
|
+
return kebab
|
|
23
|
+
.split('-')
|
|
24
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
25
|
+
.join('');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Converts PascalCase parameter name to kebab-case CLI option name.
|
|
30
|
+
* e.g., "ForceRecompile" -> "force-recompile"
|
|
31
|
+
*/
|
|
32
|
+
export function pascalToKebabCase(pascal: string): string {
|
|
33
|
+
const kebab = pascal.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
34
|
+
return kebab.startsWith('-') ? kebab.slice(1) : kebab;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parses CLI arguments into tool parameters based on the tool schema.
|
|
39
|
+
*/
|
|
40
|
+
export function parseToolArgs(
|
|
41
|
+
args: string[],
|
|
42
|
+
schema: ToolSchema,
|
|
43
|
+
cliOptions: Record<string, unknown>,
|
|
44
|
+
): Record<string, unknown> {
|
|
45
|
+
const params: Record<string, unknown> = {};
|
|
46
|
+
|
|
47
|
+
for (const [paramName, paramInfo] of Object.entries(schema.properties)) {
|
|
48
|
+
const kebabName = pascalToKebabCase(paramName);
|
|
49
|
+
const cliValue = cliOptions[kebabName];
|
|
50
|
+
|
|
51
|
+
if (cliValue === undefined) {
|
|
52
|
+
if (paramInfo.DefaultValue !== undefined) {
|
|
53
|
+
params[paramName] = paramInfo.DefaultValue;
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
params[paramName] = convertValue(cliValue, paramInfo.Type);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return params;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function convertValue(value: unknown, type: string): unknown {
|
|
65
|
+
if (value === undefined || value === null) {
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const lowerType = type.toLowerCase();
|
|
70
|
+
|
|
71
|
+
switch (lowerType) {
|
|
72
|
+
case 'boolean':
|
|
73
|
+
if (typeof value === 'boolean') {
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
if (typeof value === 'string') {
|
|
77
|
+
return value.toLowerCase() === 'true';
|
|
78
|
+
}
|
|
79
|
+
return Boolean(value);
|
|
80
|
+
|
|
81
|
+
case 'number':
|
|
82
|
+
case 'integer':
|
|
83
|
+
if (typeof value === 'number') {
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
if (typeof value === 'string') {
|
|
87
|
+
const parsed = parseFloat(value);
|
|
88
|
+
return isNaN(parsed) ? 0 : parsed;
|
|
89
|
+
}
|
|
90
|
+
return Number(value);
|
|
91
|
+
|
|
92
|
+
case 'string':
|
|
93
|
+
if (typeof value === 'string') {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
return JSON.stringify(value);
|
|
100
|
+
|
|
101
|
+
case 'array':
|
|
102
|
+
if (Array.isArray(value)) {
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
if (typeof value === 'string') {
|
|
106
|
+
return value.split(',').map((s) => s.trim());
|
|
107
|
+
}
|
|
108
|
+
return [value];
|
|
109
|
+
|
|
110
|
+
default:
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Generates commander.js option string from parameter info.
|
|
117
|
+
* e.g., "--force-recompile" for boolean, "--max-count <value>" for others
|
|
118
|
+
*/
|
|
119
|
+
export function generateOptionString(paramName: string, paramInfo: ToolParameter): string {
|
|
120
|
+
const kebabName = pascalToKebabCase(paramName);
|
|
121
|
+
const lowerType = paramInfo.Type.toLowerCase();
|
|
122
|
+
|
|
123
|
+
if (lowerType === 'boolean') {
|
|
124
|
+
return `--${kebabName}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return `--${kebabName} <value>`;
|
|
128
|
+
}
|