uloop-cli 0.67.4 → 0.68.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 +6 -6
- package/README_ja.md +6 -6
- package/dist/cli.bundle.cjs +206 -150
- package/dist/cli.bundle.cjs.map +4 -4
- package/package.json +3 -3
- package/src/__tests__/tool-settings-loader.test.ts +260 -0
- package/src/cli.ts +38 -10
- package/src/default-tools.json +8 -30
- package/src/skills/deprecated-skills.ts +1 -0
- package/src/skills/skills-manager.ts +28 -2
- package/src/tool-settings-loader.ts +74 -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.68.0",
|
|
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",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"devDependencies": {
|
|
49
49
|
"@eslint/js": "10.0.1",
|
|
50
50
|
"@types/jest": "30.0.0",
|
|
51
|
-
"@types/node": "25.3.
|
|
51
|
+
"@types/node": "25.3.2",
|
|
52
52
|
"@types/semver": "7.7.1",
|
|
53
53
|
"esbuild": "0.27.3",
|
|
54
54
|
"eslint": "10.0.2",
|
|
@@ -63,6 +63,6 @@
|
|
|
63
63
|
"typescript-eslint": "8.56.1"
|
|
64
64
|
},
|
|
65
65
|
"overrides": {
|
|
66
|
-
"minimatch": "10.2.
|
|
66
|
+
"minimatch": "10.2.4"
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for tool-settings-loader.ts
|
|
3
|
+
*
|
|
4
|
+
* Tests pure functions for loading and filtering disabled tools.
|
|
5
|
+
* Uses temporary directories to avoid affecting real project settings.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mkdirSync, rmSync, writeFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { tmpdir } from 'os';
|
|
11
|
+
|
|
12
|
+
// Mock findUnityProjectRoot before importing the module
|
|
13
|
+
let mockProjectRoot: string | null = null;
|
|
14
|
+
|
|
15
|
+
jest.mock('../project-root.js', () => ({
|
|
16
|
+
findUnityProjectRoot: (): string | null => mockProjectRoot,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// Import after mocking
|
|
20
|
+
import { loadDisabledTools, isToolEnabled, filterEnabledTools } from '../tool-settings-loader.js';
|
|
21
|
+
import type { ToolDefinition } from '../tool-cache.js';
|
|
22
|
+
|
|
23
|
+
describe('tool-settings-loader', () => {
|
|
24
|
+
let testDir: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
testDir = join(tmpdir(), `uloop-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
28
|
+
mkdirSync(join(testDir, '.uloop'), { recursive: true });
|
|
29
|
+
mockProjectRoot = testDir;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
34
|
+
mockProjectRoot = null;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ── loadDisabledTools ──────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
describe('loadDisabledTools', () => {
|
|
40
|
+
it('should return disabled tools from valid settings file', () => {
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(testDir, '.uloop', 'settings.tools.json'),
|
|
43
|
+
JSON.stringify({ disabledTools: ['compile', 'get-logs'] }),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const result: string[] = loadDisabledTools();
|
|
47
|
+
|
|
48
|
+
expect(result).toEqual(['compile', 'get-logs']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should return empty array when file does not exist', () => {
|
|
52
|
+
const result: string[] = loadDisabledTools();
|
|
53
|
+
|
|
54
|
+
expect(result).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should return empty array when project root is null', () => {
|
|
58
|
+
mockProjectRoot = null;
|
|
59
|
+
|
|
60
|
+
const result: string[] = loadDisabledTools();
|
|
61
|
+
|
|
62
|
+
expect(result).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should return empty array for invalid JSON', () => {
|
|
66
|
+
writeFileSync(join(testDir, '.uloop', 'settings.tools.json'), '{invalid json}');
|
|
67
|
+
|
|
68
|
+
const result: string[] = loadDisabledTools();
|
|
69
|
+
|
|
70
|
+
expect(result).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return empty array for empty file', () => {
|
|
74
|
+
writeFileSync(join(testDir, '.uloop', 'settings.tools.json'), '');
|
|
75
|
+
|
|
76
|
+
const result: string[] = loadDisabledTools();
|
|
77
|
+
|
|
78
|
+
expect(result).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should return empty array when disabledTools is not an array', () => {
|
|
82
|
+
writeFileSync(
|
|
83
|
+
join(testDir, '.uloop', 'settings.tools.json'),
|
|
84
|
+
JSON.stringify({ disabledTools: 'not-an-array' }),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const result: string[] = loadDisabledTools();
|
|
88
|
+
|
|
89
|
+
expect(result).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return empty array when disabledTools key is missing', () => {
|
|
93
|
+
writeFileSync(
|
|
94
|
+
join(testDir, '.uloop', 'settings.tools.json'),
|
|
95
|
+
JSON.stringify({ other: 'data' }),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const result: string[] = loadDisabledTools();
|
|
99
|
+
|
|
100
|
+
expect(result).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ── isToolEnabled ──────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
describe('isToolEnabled', () => {
|
|
107
|
+
it('should return false for a disabled tool', () => {
|
|
108
|
+
writeFileSync(
|
|
109
|
+
join(testDir, '.uloop', 'settings.tools.json'),
|
|
110
|
+
JSON.stringify({ disabledTools: ['compile'] }),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(isToolEnabled('compile')).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should return true for an enabled tool', () => {
|
|
117
|
+
writeFileSync(
|
|
118
|
+
join(testDir, '.uloop', 'settings.tools.json'),
|
|
119
|
+
JSON.stringify({ disabledTools: ['compile'] }),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(isToolEnabled('get-logs')).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return true when no settings file exists', () => {
|
|
126
|
+
expect(isToolEnabled('compile')).toBe(true);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ── filterEnabledTools ─────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
describe('filterEnabledTools', () => {
|
|
133
|
+
const mockTools: ToolDefinition[] = [
|
|
134
|
+
{
|
|
135
|
+
name: 'compile',
|
|
136
|
+
description: 'Compile',
|
|
137
|
+
inputSchema: { type: 'object', properties: {} },
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'get-logs',
|
|
141
|
+
description: 'Get logs',
|
|
142
|
+
inputSchema: { type: 'object', properties: {} },
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'clear-console',
|
|
146
|
+
description: 'Clear',
|
|
147
|
+
inputSchema: { type: 'object', properties: {} },
|
|
148
|
+
},
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
it('should filter out disabled tools', () => {
|
|
152
|
+
writeFileSync(
|
|
153
|
+
join(testDir, '.uloop', 'settings.tools.json'),
|
|
154
|
+
JSON.stringify({ disabledTools: ['compile', 'clear-console'] }),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const result: ToolDefinition[] = filterEnabledTools(mockTools);
|
|
158
|
+
|
|
159
|
+
expect(result).toHaveLength(1);
|
|
160
|
+
expect(result[0].name).toBe('get-logs');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('should return all tools when none are disabled', () => {
|
|
164
|
+
writeFileSync(
|
|
165
|
+
join(testDir, '.uloop', 'settings.tools.json'),
|
|
166
|
+
JSON.stringify({ disabledTools: [] }),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const result: ToolDefinition[] = filterEnabledTools(mockTools);
|
|
170
|
+
|
|
171
|
+
expect(result).toHaveLength(3);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('should return all tools when settings file does not exist', () => {
|
|
175
|
+
const result: ToolDefinition[] = filterEnabledTools(mockTools);
|
|
176
|
+
|
|
177
|
+
expect(result).toHaveLength(3);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ── projectPath override ────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
describe('projectPath override', () => {
|
|
184
|
+
let otherDir: string;
|
|
185
|
+
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
otherDir = join(tmpdir(), `uloop-other-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
188
|
+
mkdirSync(join(otherDir, '.uloop'), { recursive: true });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
afterEach(() => {
|
|
192
|
+
rmSync(otherDir, { recursive: true, force: true });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should load settings from projectPath instead of cwd project root', () => {
|
|
196
|
+
writeFileSync(
|
|
197
|
+
join(otherDir, '.uloop', 'settings.tools.json'),
|
|
198
|
+
JSON.stringify({ disabledTools: ['get-logs'] }),
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const result: string[] = loadDisabledTools(otherDir);
|
|
202
|
+
|
|
203
|
+
expect(result).toEqual(['get-logs']);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should ignore cwd project root when projectPath is provided', () => {
|
|
207
|
+
writeFileSync(
|
|
208
|
+
join(testDir, '.uloop', 'settings.tools.json'),
|
|
209
|
+
JSON.stringify({ disabledTools: ['compile'] }),
|
|
210
|
+
);
|
|
211
|
+
writeFileSync(
|
|
212
|
+
join(otherDir, '.uloop', 'settings.tools.json'),
|
|
213
|
+
JSON.stringify({ disabledTools: ['get-logs'] }),
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
expect(loadDisabledTools(otherDir)).toEqual(['get-logs']);
|
|
217
|
+
expect(loadDisabledTools()).toEqual(['compile']);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should pass projectPath through isToolEnabled', () => {
|
|
221
|
+
writeFileSync(
|
|
222
|
+
join(otherDir, '.uloop', 'settings.tools.json'),
|
|
223
|
+
JSON.stringify({ disabledTools: ['compile'] }),
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(isToolEnabled('compile', otherDir)).toBe(false);
|
|
227
|
+
expect(isToolEnabled('get-logs', otherDir)).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should pass projectPath through filterEnabledTools', () => {
|
|
231
|
+
const mockTools: ToolDefinition[] = [
|
|
232
|
+
{
|
|
233
|
+
name: 'compile',
|
|
234
|
+
description: 'Compile',
|
|
235
|
+
inputSchema: { type: 'object', properties: {} },
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'get-logs',
|
|
239
|
+
description: 'Get logs',
|
|
240
|
+
inputSchema: { type: 'object', properties: {} },
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
writeFileSync(
|
|
244
|
+
join(otherDir, '.uloop', 'settings.tools.json'),
|
|
245
|
+
JSON.stringify({ disabledTools: ['compile'] }),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
const result: ToolDefinition[] = filterEnabledTools(mockTools, otherDir);
|
|
249
|
+
|
|
250
|
+
expect(result).toHaveLength(1);
|
|
251
|
+
expect(result[0].name).toBe('get-logs');
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('should return empty array when projectPath has no settings file', () => {
|
|
255
|
+
const result: string[] = loadDisabledTools(otherDir);
|
|
256
|
+
|
|
257
|
+
expect(result).toEqual([]);
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
});
|
package/src/cli.ts
CHANGED
|
@@ -35,6 +35,7 @@ import { registerFocusWindowCommand } from './commands/focus-window.js';
|
|
|
35
35
|
import { VERSION } from './version.js';
|
|
36
36
|
import { findUnityProjectRoot } from './project-root.js';
|
|
37
37
|
import { validateProjectPath } from './port-resolver.js';
|
|
38
|
+
import { filterEnabledTools, isToolEnabled } from './tool-settings-loader.js';
|
|
38
39
|
|
|
39
40
|
interface CliOptions extends GlobalOptions {
|
|
40
41
|
[key: string]: unknown;
|
|
@@ -312,6 +313,11 @@ function isConnectionError(message: string): boolean {
|
|
|
312
313
|
return message.includes('ECONNREFUSED') || message.includes('EADDRNOTAVAIL');
|
|
313
314
|
}
|
|
314
315
|
|
|
316
|
+
function printToolDisabledError(cmdName: string): void {
|
|
317
|
+
console.error(`\x1b[33mTool '${cmdName}' is disabled.\x1b[0m`);
|
|
318
|
+
console.error('You can enable it in Unity: Window > uLoop > Tool Settings');
|
|
319
|
+
}
|
|
320
|
+
|
|
315
321
|
function printConnectionError(): void {
|
|
316
322
|
console.error('\x1b[31mError: Cannot connect to Unity.\x1b[0m');
|
|
317
323
|
console.error('Make sure Unity Editor is open and uLoopMCP server is running.');
|
|
@@ -707,10 +713,15 @@ function handleCompletion(install: boolean, shellOverride?: string): void {
|
|
|
707
713
|
*/
|
|
708
714
|
function handleCompletionOptions(): boolean {
|
|
709
715
|
const args = process.argv.slice(2);
|
|
716
|
+
const projectPath: string | undefined = extractSyncGlobalOptions(args).projectPath;
|
|
710
717
|
|
|
711
718
|
if (args.includes('--list-commands')) {
|
|
712
719
|
const tools = loadToolsCache();
|
|
713
|
-
|
|
720
|
+
// Only filter disabled tools when --project-path is explicitly provided;
|
|
721
|
+
// without it, loadDisabledTools would trigger findUnityProjectRoot() scanning
|
|
722
|
+
const enabledTools: ToolDefinition[] =
|
|
723
|
+
projectPath !== undefined ? filterEnabledTools(tools.tools, projectPath) : tools.tools;
|
|
724
|
+
const allCommands = [...BUILTIN_COMMANDS, ...enabledTools.map((t) => t.name)];
|
|
714
725
|
console.log(allCommands.join('\n'));
|
|
715
726
|
return true;
|
|
716
727
|
}
|
|
@@ -718,7 +729,7 @@ function handleCompletionOptions(): boolean {
|
|
|
718
729
|
const listOptionsIdx = args.indexOf('--list-options');
|
|
719
730
|
if (listOptionsIdx !== -1 && args[listOptionsIdx + 1]) {
|
|
720
731
|
const cmdName = args[listOptionsIdx + 1];
|
|
721
|
-
listOptionsForCommand(cmdName);
|
|
732
|
+
listOptionsForCommand(cmdName, projectPath);
|
|
722
733
|
return true;
|
|
723
734
|
}
|
|
724
735
|
|
|
@@ -728,15 +739,20 @@ function handleCompletionOptions(): boolean {
|
|
|
728
739
|
/**
|
|
729
740
|
* List options for a specific command.
|
|
730
741
|
*/
|
|
731
|
-
function listOptionsForCommand(cmdName: string): void {
|
|
742
|
+
function listOptionsForCommand(cmdName: string, projectPath?: string): void {
|
|
732
743
|
// Built-in commands have no tool-specific options
|
|
733
744
|
if (BUILTIN_COMMANDS.includes(cmdName as (typeof BUILTIN_COMMANDS)[number])) {
|
|
734
745
|
return;
|
|
735
746
|
}
|
|
736
747
|
|
|
737
748
|
// Tool commands - only output tool-specific options
|
|
749
|
+
// Only filter disabled tools when --project-path is explicitly provided;
|
|
750
|
+
// without it, loadDisabledTools would trigger findUnityProjectRoot() scanning
|
|
738
751
|
const tools = loadToolsCache();
|
|
739
|
-
const tool
|
|
752
|
+
const tool: ToolDefinition | undefined =
|
|
753
|
+
projectPath !== undefined
|
|
754
|
+
? filterEnabledTools(tools.tools, projectPath).find((t) => t.name === cmdName)
|
|
755
|
+
: tools.tools.find((t) => t.name === cmdName);
|
|
740
756
|
if (!tool) {
|
|
741
757
|
return;
|
|
742
758
|
}
|
|
@@ -753,12 +769,12 @@ function listOptionsForCommand(cmdName: string): void {
|
|
|
753
769
|
/**
|
|
754
770
|
* Check if a command exists in the current program.
|
|
755
771
|
*/
|
|
756
|
-
function commandExists(cmdName: string): boolean {
|
|
772
|
+
function commandExists(cmdName: string, projectPath?: string): boolean {
|
|
757
773
|
if (BUILTIN_COMMANDS.includes(cmdName as (typeof BUILTIN_COMMANDS)[number])) {
|
|
758
774
|
return true;
|
|
759
775
|
}
|
|
760
776
|
const tools = loadToolsCache();
|
|
761
|
-
return tools.tools.some((t) => t.name === cmdName);
|
|
777
|
+
return filterEnabledTools(tools.tools, projectPath).some((t) => t.name === cmdName);
|
|
762
778
|
}
|
|
763
779
|
|
|
764
780
|
function shouldSkipAutoSync(cmdName: string | undefined, args: string[]): boolean {
|
|
@@ -822,7 +838,13 @@ async function main(): Promise<void> {
|
|
|
822
838
|
|
|
823
839
|
if (skipProjectDetection) {
|
|
824
840
|
const defaultTools = getDefaultTools();
|
|
825
|
-
|
|
841
|
+
// Only filter disabled tools when --project-path is explicitly provided;
|
|
842
|
+
// without it, filterEnabledTools would trigger findUnityProjectRoot() scanning
|
|
843
|
+
const tools: ToolDefinition[] =
|
|
844
|
+
syncGlobalOptions.projectPath !== undefined
|
|
845
|
+
? filterEnabledTools(defaultTools.tools, syncGlobalOptions.projectPath)
|
|
846
|
+
: defaultTools.tools;
|
|
847
|
+
for (const tool of tools) {
|
|
826
848
|
registerToolCommand(tool);
|
|
827
849
|
}
|
|
828
850
|
program.parse();
|
|
@@ -855,16 +877,22 @@ async function main(): Promise<void> {
|
|
|
855
877
|
|
|
856
878
|
// Register tool commands from cache (after potential auto-sync)
|
|
857
879
|
const toolsCache = loadToolsCache();
|
|
858
|
-
|
|
880
|
+
const projectPath: string | undefined = syncGlobalOptions.projectPath;
|
|
881
|
+
for (const tool of filterEnabledTools(toolsCache.tools, projectPath)) {
|
|
859
882
|
registerToolCommand(tool);
|
|
860
883
|
}
|
|
861
884
|
|
|
862
|
-
if (cmdName && !commandExists(cmdName)) {
|
|
885
|
+
if (cmdName && !commandExists(cmdName, projectPath)) {
|
|
886
|
+
if (!isToolEnabled(cmdName, projectPath)) {
|
|
887
|
+
printToolDisabledError(cmdName);
|
|
888
|
+
process.exit(1);
|
|
889
|
+
}
|
|
890
|
+
|
|
863
891
|
console.log(`\x1b[33mUnknown command '${cmdName}'. Syncing tools from Unity...\x1b[0m`);
|
|
864
892
|
try {
|
|
865
893
|
await syncTools(syncGlobalOptions);
|
|
866
894
|
const newCache = loadToolsCache();
|
|
867
|
-
const tool = newCache.tools.find((t) => t.name === cmdName);
|
|
895
|
+
const tool = filterEnabledTools(newCache.tools, projectPath).find((t) => t.name === cmdName);
|
|
868
896
|
if (tool) {
|
|
869
897
|
registerToolCommand(tool);
|
|
870
898
|
console.log(`\x1b[32m✓ Found '${cmdName}' after sync.\x1b[0m\n`);
|
package/src/default-tools.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.68.0",
|
|
3
3
|
"tools": [
|
|
4
4
|
{
|
|
5
5
|
"name": "compile",
|
|
@@ -343,8 +343,8 @@
|
|
|
343
343
|
}
|
|
344
344
|
},
|
|
345
345
|
{
|
|
346
|
-
"name": "
|
|
347
|
-
"description": "
|
|
346
|
+
"name": "screenshot",
|
|
347
|
+
"description": "Take a screenshot of Unity EditorWindow and save as PNG",
|
|
348
348
|
"inputSchema": {
|
|
349
349
|
"type": "object",
|
|
350
350
|
"properties": {
|
|
@@ -367,6 +367,11 @@
|
|
|
367
367
|
"contains"
|
|
368
368
|
],
|
|
369
369
|
"default": "exact"
|
|
370
|
+
},
|
|
371
|
+
"OutputDirectory": {
|
|
372
|
+
"type": "string",
|
|
373
|
+
"description": "Output directory path for saving screenshots. When empty, uses default path (.uloop/outputs/Screenshots/). Accepts absolute paths.",
|
|
374
|
+
"default": ""
|
|
370
375
|
}
|
|
371
376
|
}
|
|
372
377
|
}
|
|
@@ -392,33 +397,6 @@
|
|
|
392
397
|
}
|
|
393
398
|
}
|
|
394
399
|
},
|
|
395
|
-
{
|
|
396
|
-
"name": "get-provider-details",
|
|
397
|
-
"description": "Get Unity Search provider details",
|
|
398
|
-
"inputSchema": {
|
|
399
|
-
"type": "object",
|
|
400
|
-
"properties": {
|
|
401
|
-
"ProviderId": {
|
|
402
|
-
"type": "string",
|
|
403
|
-
"description": "Specific provider ID"
|
|
404
|
-
},
|
|
405
|
-
"ActiveOnly": {
|
|
406
|
-
"type": "boolean",
|
|
407
|
-
"description": "Only active providers"
|
|
408
|
-
},
|
|
409
|
-
"IncludeDescriptions": {
|
|
410
|
-
"type": "boolean",
|
|
411
|
-
"description": "Include descriptions",
|
|
412
|
-
"default": true
|
|
413
|
-
},
|
|
414
|
-
"SortByPriority": {
|
|
415
|
-
"type": "boolean",
|
|
416
|
-
"description": "Sort by priority",
|
|
417
|
-
"default": true
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
},
|
|
422
400
|
{
|
|
423
401
|
"name": "get-project-info",
|
|
424
402
|
"description": "Get Unity project information",
|
|
@@ -13,6 +13,7 @@ import { homedir } from 'os';
|
|
|
13
13
|
import { TargetConfig } from './target-config.js';
|
|
14
14
|
import { findUnityProjectRoot, getUnityProjectStatus } from '../project-root.js';
|
|
15
15
|
import { DEPRECATED_SKILLS } from './deprecated-skills.js';
|
|
16
|
+
import { loadDisabledTools } from '../tool-settings-loader.js';
|
|
16
17
|
|
|
17
18
|
export type SkillStatus = 'installed' | 'not_installed' | 'outdated';
|
|
18
19
|
|
|
@@ -25,6 +26,7 @@ export interface SkillInfo {
|
|
|
25
26
|
|
|
26
27
|
export interface SkillDefinition {
|
|
27
28
|
name: string;
|
|
29
|
+
toolName?: string;
|
|
28
30
|
dirName: string;
|
|
29
31
|
content: string;
|
|
30
32
|
sourcePath: string;
|
|
@@ -145,13 +147,13 @@ export function getSkillStatus(
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
export function parseFrontmatter(content: string): Record<string, string | boolean> {
|
|
148
|
-
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
150
|
+
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
149
151
|
if (!frontmatterMatch) {
|
|
150
152
|
return {};
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
const frontmatterMap = new Map<string, string | boolean>();
|
|
154
|
-
const lines = frontmatterMatch[1].split(
|
|
156
|
+
const lines = frontmatterMatch[1].split(/\r?\n/);
|
|
155
157
|
|
|
156
158
|
for (const line of lines) {
|
|
157
159
|
const colonIndex = line.indexOf(':');
|
|
@@ -239,10 +241,13 @@ function scanEditorFolderForSkills(
|
|
|
239
241
|
}
|
|
240
242
|
|
|
241
243
|
const name = typeof frontmatter.name === 'string' ? frontmatter.name : entry.name;
|
|
244
|
+
const toolName =
|
|
245
|
+
typeof frontmatter.toolName === 'string' ? frontmatter.toolName : undefined;
|
|
242
246
|
const additionalFiles = collectSkillFolderFiles(skillDir);
|
|
243
247
|
|
|
244
248
|
skills.push({
|
|
245
249
|
name,
|
|
250
|
+
toolName,
|
|
246
251
|
dirName: name,
|
|
247
252
|
content,
|
|
248
253
|
sourcePath: skillMdPath,
|
|
@@ -399,11 +404,19 @@ export function installAllSkills(target: TargetConfig, global: boolean): Install
|
|
|
399
404
|
}
|
|
400
405
|
}
|
|
401
406
|
|
|
407
|
+
// Global installs ignore project-local tool settings
|
|
408
|
+
const disabledTools: string[] = global ? [] : loadDisabledTools();
|
|
409
|
+
|
|
402
410
|
const allSkills = collectAllSkills();
|
|
403
411
|
const projectSkills = allSkills.filter((skill) => skill.sourceType === 'project');
|
|
404
412
|
const nonProjectSkills = allSkills.filter((skill) => skill.sourceType !== 'project');
|
|
405
413
|
|
|
406
414
|
for (const skill of allSkills) {
|
|
415
|
+
if (isSkillDisabledByToolSettings(skill, disabledTools)) {
|
|
416
|
+
uninstallSkill(skill, target, global);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
407
420
|
const status = getSkillStatus(skill, target, global);
|
|
408
421
|
|
|
409
422
|
if (status === 'not_installed') {
|
|
@@ -422,6 +435,19 @@ export function installAllSkills(target: TargetConfig, global: boolean): Install
|
|
|
422
435
|
return result;
|
|
423
436
|
}
|
|
424
437
|
|
|
438
|
+
function isSkillDisabledByToolSettings(skill: SkillDefinition, disabledTools: string[]): boolean {
|
|
439
|
+
if (disabledTools.length === 0) {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const toolName: string | null =
|
|
444
|
+
skill.toolName ?? (skill.name.startsWith('uloop-') ? skill.name.slice('uloop-'.length) : null);
|
|
445
|
+
if (toolName === null) {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
return disabledTools.includes(toolName);
|
|
449
|
+
}
|
|
450
|
+
|
|
425
451
|
export interface UninstallResult {
|
|
426
452
|
removed: number;
|
|
427
453
|
notFound: number;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads tool toggle settings from .uloop/settings.tools.json.
|
|
3
|
+
* Used by CLI to filter out disabled tools from help and command registration.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// File paths are constructed from Unity project root, not from untrusted user input
|
|
7
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
8
|
+
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { join, resolve } from 'path';
|
|
11
|
+
import { findUnityProjectRoot } from './project-root.js';
|
|
12
|
+
import type { ToolDefinition } from './tool-cache.js';
|
|
13
|
+
|
|
14
|
+
const ULOOP_DIR = '.uloop';
|
|
15
|
+
const TOOL_SETTINGS_FILE = 'settings.tools.json';
|
|
16
|
+
|
|
17
|
+
interface ToolSettingsData {
|
|
18
|
+
disabledTools: string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function loadDisabledTools(projectPath?: string): string[] {
|
|
22
|
+
const projectRoot: string | null =
|
|
23
|
+
projectPath !== undefined ? resolve(projectPath) : findUnityProjectRoot();
|
|
24
|
+
if (projectRoot === null) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const settingsPath: string = join(projectRoot, ULOOP_DIR, TOOL_SETTINGS_FILE);
|
|
29
|
+
|
|
30
|
+
let content: string;
|
|
31
|
+
try {
|
|
32
|
+
content = readFileSync(settingsPath, 'utf-8');
|
|
33
|
+
} catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!content.trim()) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let parsed: unknown;
|
|
42
|
+
try {
|
|
43
|
+
parsed = JSON.parse(content);
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const data = parsed as ToolSettingsData;
|
|
53
|
+
if (!Array.isArray(data.disabledTools)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return data.disabledTools;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isToolEnabled(toolName: string, projectPath?: string): boolean {
|
|
61
|
+
const disabledTools: string[] = loadDisabledTools(projectPath);
|
|
62
|
+
return !disabledTools.includes(toolName);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function filterEnabledTools(
|
|
66
|
+
tools: ToolDefinition[],
|
|
67
|
+
projectPath?: string,
|
|
68
|
+
): ToolDefinition[] {
|
|
69
|
+
const disabledTools: string[] = loadDisabledTools(projectPath);
|
|
70
|
+
if (disabledTools.length === 0) {
|
|
71
|
+
return tools;
|
|
72
|
+
}
|
|
73
|
+
return tools.filter((tool) => !disabledTools.includes(tool.name));
|
|
74
|
+
}
|
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.68.0'; // x-release-please-version
|