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
package/src/cli.ts
ADDED
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI entry point for uloop command.
|
|
3
|
+
* Provides direct Unity communication without MCP server.
|
|
4
|
+
* Commands are dynamically registered from tools.json cache.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
|
+
import { join, basename, dirname } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import {
|
|
13
|
+
executeToolCommand,
|
|
14
|
+
listAvailableTools,
|
|
15
|
+
GlobalOptions,
|
|
16
|
+
syncTools,
|
|
17
|
+
} from './execute-tool.js';
|
|
18
|
+
import { loadToolsCache, ToolDefinition, ToolProperty } from './tool-cache.js';
|
|
19
|
+
import { pascalToKebabCase } from './arg-parser.js';
|
|
20
|
+
import { registerSkillsCommand } from './skills/skills-command.js';
|
|
21
|
+
import { VERSION } from './version.js';
|
|
22
|
+
import { findUnityProjectRoot } from './project-root.js';
|
|
23
|
+
|
|
24
|
+
interface CliOptions extends GlobalOptions {
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const BUILTIN_COMMANDS = ['list', 'sync', 'completion', 'update', 'skills'] as const;
|
|
29
|
+
|
|
30
|
+
const program = new Command();
|
|
31
|
+
|
|
32
|
+
program
|
|
33
|
+
.name('uloop')
|
|
34
|
+
.description('Unity MCP CLI - Direct communication with Unity Editor')
|
|
35
|
+
.version(VERSION, '-v, --version', 'Output the version number');
|
|
36
|
+
|
|
37
|
+
// --list-commands: Output command names for shell completion
|
|
38
|
+
program.option('--list-commands', 'List all command names (for shell completion)');
|
|
39
|
+
|
|
40
|
+
// --list-options <cmd>: Output options for a specific command (for shell completion)
|
|
41
|
+
program.option('--list-options <cmd>', 'List options for a command (for shell completion)');
|
|
42
|
+
|
|
43
|
+
// Built-in commands (not from tools.json)
|
|
44
|
+
program
|
|
45
|
+
.command('list')
|
|
46
|
+
.description('List all available tools from Unity')
|
|
47
|
+
.option('-p, --port <port>', 'Unity TCP port')
|
|
48
|
+
.action(async (options: CliOptions) => {
|
|
49
|
+
await runWithErrorHandling(() => listAvailableTools(options));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command('sync')
|
|
54
|
+
.description('Sync tool definitions from Unity to local cache')
|
|
55
|
+
.option('-p, --port <port>', 'Unity TCP port')
|
|
56
|
+
.action(async (options: CliOptions) => {
|
|
57
|
+
await runWithErrorHandling(() => syncTools(options));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
program
|
|
61
|
+
.command('completion')
|
|
62
|
+
.description('Setup shell completion')
|
|
63
|
+
.option('--install', 'Install completion to shell config file')
|
|
64
|
+
.option('--shell <type>', 'Shell type: bash, zsh, or powershell')
|
|
65
|
+
.action((options: { install?: boolean; shell?: string }) => {
|
|
66
|
+
handleCompletion(options.install ?? false, options.shell);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
program
|
|
70
|
+
.command('update')
|
|
71
|
+
.description('Update uloop CLI to the latest version')
|
|
72
|
+
.action(() => {
|
|
73
|
+
updateCli();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Register skills subcommand
|
|
77
|
+
registerSkillsCommand(program);
|
|
78
|
+
|
|
79
|
+
// Load tools from cache and register commands dynamically
|
|
80
|
+
const toolsCache = loadToolsCache();
|
|
81
|
+
for (const tool of toolsCache.tools) {
|
|
82
|
+
registerToolCommand(tool);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Register a tool as a CLI command dynamically.
|
|
87
|
+
*/
|
|
88
|
+
function registerToolCommand(tool: ToolDefinition): void {
|
|
89
|
+
const cmd = program.command(tool.name).description(tool.description);
|
|
90
|
+
|
|
91
|
+
// Add options from inputSchema.properties
|
|
92
|
+
const properties = tool.inputSchema.properties;
|
|
93
|
+
for (const [propName, propInfo] of Object.entries(properties)) {
|
|
94
|
+
const optionStr = generateOptionString(propName, propInfo);
|
|
95
|
+
const description = buildOptionDescription(propInfo);
|
|
96
|
+
const defaultValue = propInfo.default as string | boolean | undefined;
|
|
97
|
+
if (defaultValue !== undefined) {
|
|
98
|
+
cmd.option(optionStr, description, defaultValue);
|
|
99
|
+
} else {
|
|
100
|
+
cmd.option(optionStr, description);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Add global options
|
|
105
|
+
cmd.option('-p, --port <port>', 'Unity TCP port');
|
|
106
|
+
|
|
107
|
+
cmd.action(async (options: CliOptions) => {
|
|
108
|
+
const params = buildParams(options, properties);
|
|
109
|
+
|
|
110
|
+
// Unescape \! to ! for execute-dynamic-code
|
|
111
|
+
// Some shells (e.g., Claude Code's bash wrapper) escape ! as \!
|
|
112
|
+
if (tool.name === 'execute-dynamic-code' && params['Code']) {
|
|
113
|
+
const code = params['Code'] as string;
|
|
114
|
+
params['Code'] = code.replace(/\\!/g, '!');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await runWithErrorHandling(() =>
|
|
118
|
+
executeToolCommand(tool.name, params, extractGlobalOptions(options)),
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Generate commander.js option string from property info.
|
|
125
|
+
*/
|
|
126
|
+
function generateOptionString(propName: string, propInfo: ToolProperty): string {
|
|
127
|
+
const kebabName = pascalToKebabCase(propName);
|
|
128
|
+
const lowerType = propInfo.type.toLowerCase();
|
|
129
|
+
|
|
130
|
+
if (lowerType === 'boolean') {
|
|
131
|
+
return `--${kebabName}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `--${kebabName} <value>`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Build option description with enum values if present.
|
|
139
|
+
*/
|
|
140
|
+
function buildOptionDescription(propInfo: ToolProperty): string {
|
|
141
|
+
let desc = propInfo.description || '';
|
|
142
|
+
if (propInfo.enum && propInfo.enum.length > 0) {
|
|
143
|
+
desc += ` (${propInfo.enum.join(', ')})`;
|
|
144
|
+
}
|
|
145
|
+
return desc;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Build parameters from CLI options.
|
|
150
|
+
*/
|
|
151
|
+
function buildParams(
|
|
152
|
+
options: Record<string, unknown>,
|
|
153
|
+
properties: Record<string, ToolProperty>,
|
|
154
|
+
): Record<string, unknown> {
|
|
155
|
+
const params: Record<string, unknown> = {};
|
|
156
|
+
|
|
157
|
+
for (const propName of Object.keys(properties)) {
|
|
158
|
+
const camelName = propName.charAt(0).toLowerCase() + propName.slice(1);
|
|
159
|
+
const value = options[camelName];
|
|
160
|
+
|
|
161
|
+
if (value !== undefined) {
|
|
162
|
+
const propInfo = properties[propName];
|
|
163
|
+
params[propName] = convertValue(value, propInfo);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return params;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Convert CLI value to appropriate type based on property info.
|
|
172
|
+
*/
|
|
173
|
+
function convertValue(value: unknown, propInfo: ToolProperty): unknown {
|
|
174
|
+
const lowerType = propInfo.type.toLowerCase();
|
|
175
|
+
|
|
176
|
+
if (lowerType === 'array' && typeof value === 'string') {
|
|
177
|
+
return value.split(',').map((s) => s.trim());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (lowerType === 'integer' && typeof value === 'string') {
|
|
181
|
+
const parsed = parseInt(value, 10);
|
|
182
|
+
if (isNaN(parsed)) {
|
|
183
|
+
throw new Error(`Invalid integer value: ${value}`);
|
|
184
|
+
}
|
|
185
|
+
return parsed;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (lowerType === 'number' && typeof value === 'string') {
|
|
189
|
+
const parsed = parseFloat(value);
|
|
190
|
+
if (isNaN(parsed)) {
|
|
191
|
+
throw new Error(`Invalid number value: ${value}`);
|
|
192
|
+
}
|
|
193
|
+
return parsed;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return value;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function extractGlobalOptions(options: Record<string, unknown>): GlobalOptions {
|
|
200
|
+
return {
|
|
201
|
+
port: options['port'] as string | undefined,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function isDomainReloadLockFilePresent(): boolean {
|
|
206
|
+
const projectRoot = findUnityProjectRoot();
|
|
207
|
+
if (projectRoot === null) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
const lockPath = join(projectRoot, 'Temp', 'domainreload.lock');
|
|
211
|
+
return existsSync(lockPath);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function runWithErrorHandling(fn: () => Promise<void>): Promise<void> {
|
|
215
|
+
try {
|
|
216
|
+
await fn();
|
|
217
|
+
} catch (error) {
|
|
218
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
219
|
+
|
|
220
|
+
if (message.includes('ECONNREFUSED')) {
|
|
221
|
+
if (isDomainReloadLockFilePresent()) {
|
|
222
|
+
console.error('\x1b[33m⏳ Unity is reloading (Domain Reload in progress).\x1b[0m');
|
|
223
|
+
console.error('Please wait a moment and try again.');
|
|
224
|
+
} else {
|
|
225
|
+
console.error('\x1b[31mError: Cannot connect to Unity.\x1b[0m');
|
|
226
|
+
console.error('Make sure Unity is running with uLoopMCP installed.');
|
|
227
|
+
}
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
console.error(`\x1b[31mError: ${message}\x1b[0m`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Detect shell type from environment.
|
|
238
|
+
*/
|
|
239
|
+
function detectShell(): 'bash' | 'zsh' | 'powershell' | null {
|
|
240
|
+
// Check $SHELL first (works for bash/zsh including MINGW64)
|
|
241
|
+
const shell = process.env['SHELL'] || '';
|
|
242
|
+
const shellName = basename(shell).replace(/\.exe$/i, ''); // Remove .exe for Windows
|
|
243
|
+
if (shellName === 'zsh') {
|
|
244
|
+
return 'zsh';
|
|
245
|
+
}
|
|
246
|
+
if (shellName === 'bash') {
|
|
247
|
+
return 'bash';
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check for PowerShell (only if $SHELL is not set)
|
|
251
|
+
if (process.env['PSModulePath']) {
|
|
252
|
+
return 'powershell';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get shell config file path.
|
|
260
|
+
*/
|
|
261
|
+
function getShellConfigPath(shell: 'bash' | 'zsh' | 'powershell'): string {
|
|
262
|
+
const home = homedir();
|
|
263
|
+
if (shell === 'zsh') {
|
|
264
|
+
return join(home, '.zshrc');
|
|
265
|
+
}
|
|
266
|
+
if (shell === 'powershell') {
|
|
267
|
+
// PowerShell profile path
|
|
268
|
+
return join(home, 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
|
|
269
|
+
}
|
|
270
|
+
return join(home, '.bashrc');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Get completion script for a shell.
|
|
275
|
+
*/
|
|
276
|
+
function getCompletionScript(shell: 'bash' | 'zsh' | 'powershell'): string {
|
|
277
|
+
if (shell === 'bash') {
|
|
278
|
+
return `# uloop bash completion
|
|
279
|
+
_uloop_completions() {
|
|
280
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
281
|
+
local cmd="\${COMP_WORDS[1]}"
|
|
282
|
+
|
|
283
|
+
if [[ \${COMP_CWORD} -eq 1 ]]; then
|
|
284
|
+
COMPREPLY=($(compgen -W "$(uloop --list-commands 2>/dev/null)" -- "\${cur}"))
|
|
285
|
+
elif [[ \${COMP_CWORD} -ge 2 ]]; then
|
|
286
|
+
COMPREPLY=($(compgen -W "$(uloop --list-options \${cmd} 2>/dev/null)" -- "\${cur}"))
|
|
287
|
+
fi
|
|
288
|
+
}
|
|
289
|
+
complete -F _uloop_completions uloop`;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (shell === 'powershell') {
|
|
293
|
+
return `# uloop PowerShell completion
|
|
294
|
+
Register-ArgumentCompleter -Native -CommandName uloop -ScriptBlock {
|
|
295
|
+
param($wordToComplete, $commandAst, $cursorPosition)
|
|
296
|
+
$commands = $commandAst.CommandElements
|
|
297
|
+
if ($commands.Count -eq 1) {
|
|
298
|
+
uloop --list-commands 2>$null | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
299
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
300
|
+
}
|
|
301
|
+
} elseif ($commands.Count -ge 2) {
|
|
302
|
+
$cmd = $commands[1].ToString()
|
|
303
|
+
uloop --list-options $cmd 2>$null | Where-Object { $_ -like "$wordToComplete*" } | ForEach-Object {
|
|
304
|
+
[System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* eslint-disable no-useless-escape */
|
|
311
|
+
return `# uloop zsh completion
|
|
312
|
+
_uloop() {
|
|
313
|
+
local -a commands
|
|
314
|
+
local -a options
|
|
315
|
+
local -a used_options
|
|
316
|
+
|
|
317
|
+
if (( CURRENT == 2 )); then
|
|
318
|
+
commands=(\${(f)"$(uloop --list-commands 2>/dev/null)"})
|
|
319
|
+
_describe 'command' commands
|
|
320
|
+
elif (( CURRENT >= 3 )); then
|
|
321
|
+
options=(\${(f)"$(uloop --list-options \${words[2]} 2>/dev/null)"})
|
|
322
|
+
used_options=(\${words:2})
|
|
323
|
+
for opt in \${used_options}; do
|
|
324
|
+
options=(\${options:#\$opt})
|
|
325
|
+
done
|
|
326
|
+
_describe 'option' options
|
|
327
|
+
fi
|
|
328
|
+
}
|
|
329
|
+
compdef _uloop uloop`;
|
|
330
|
+
/* eslint-enable no-useless-escape */
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Update uloop CLI to the latest version using npm.
|
|
335
|
+
*/
|
|
336
|
+
function updateCli(): void {
|
|
337
|
+
// eslint-disable-next-line no-console
|
|
338
|
+
console.log('Updating uloop-cli to the latest version...');
|
|
339
|
+
|
|
340
|
+
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
341
|
+
const child = spawn(npmCommand, ['install', '-g', 'uloop-cli@latest'], {
|
|
342
|
+
stdio: 'inherit',
|
|
343
|
+
shell: true,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
child.on('close', (code) => {
|
|
347
|
+
if (code === 0) {
|
|
348
|
+
// eslint-disable-next-line no-console
|
|
349
|
+
console.log('\n✅ uloop-cli has been updated successfully!');
|
|
350
|
+
// eslint-disable-next-line no-console
|
|
351
|
+
console.log('Run "uloop --version" to check the new version.');
|
|
352
|
+
} else {
|
|
353
|
+
// eslint-disable-next-line no-console
|
|
354
|
+
console.error(`\n❌ Update failed with exit code ${code}`);
|
|
355
|
+
process.exit(1);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
child.on('error', (err) => {
|
|
360
|
+
// eslint-disable-next-line no-console
|
|
361
|
+
console.error(`❌ Failed to run npm: ${err.message}`);
|
|
362
|
+
process.exit(1);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Handle completion command.
|
|
368
|
+
*/
|
|
369
|
+
function handleCompletion(install: boolean, shellOverride?: string): void {
|
|
370
|
+
let shell: 'bash' | 'zsh' | 'powershell' | null;
|
|
371
|
+
|
|
372
|
+
if (shellOverride) {
|
|
373
|
+
const normalized = shellOverride.toLowerCase();
|
|
374
|
+
if (normalized === 'bash' || normalized === 'zsh' || normalized === 'powershell') {
|
|
375
|
+
shell = normalized;
|
|
376
|
+
} else {
|
|
377
|
+
console.error(`Unknown shell: ${shellOverride}. Supported: bash, zsh, powershell`);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
} else {
|
|
381
|
+
shell = detectShell();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (!shell) {
|
|
385
|
+
console.error('Could not detect shell. Use --shell option: bash, zsh, or powershell');
|
|
386
|
+
process.exit(1);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const script = getCompletionScript(shell);
|
|
390
|
+
|
|
391
|
+
if (!install) {
|
|
392
|
+
console.log(script);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Install to shell config file
|
|
397
|
+
const configPath = getShellConfigPath(shell);
|
|
398
|
+
|
|
399
|
+
// PowerShell profile directory may not exist on fresh installations
|
|
400
|
+
const configDir = dirname(configPath);
|
|
401
|
+
if (!existsSync(configDir)) {
|
|
402
|
+
mkdirSync(configDir, { recursive: true });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Remove existing uloop completion and add new one
|
|
406
|
+
let content = '';
|
|
407
|
+
if (existsSync(configPath)) {
|
|
408
|
+
content = readFileSync(configPath, 'utf-8');
|
|
409
|
+
// Remove existing uloop completion block using markers
|
|
410
|
+
content = content.replace(
|
|
411
|
+
/\n?# >>> uloop completion >>>[\s\S]*?# <<< uloop completion <<<\n?/g,
|
|
412
|
+
'',
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Add new completion with markers
|
|
417
|
+
const startMarker = '# >>> uloop completion >>>';
|
|
418
|
+
const endMarker = '# <<< uloop completion <<<';
|
|
419
|
+
|
|
420
|
+
if (shell === 'powershell') {
|
|
421
|
+
const lineToAdd = `\n${startMarker}\n${script}\n${endMarker}\n`;
|
|
422
|
+
writeFileSync(configPath, content + lineToAdd, 'utf-8');
|
|
423
|
+
} else {
|
|
424
|
+
// Include --shell option to ensure correct shell detection
|
|
425
|
+
const evalLine = `eval "$(uloop completion --shell ${shell})"`;
|
|
426
|
+
const lineToAdd = `\n${startMarker}\n${evalLine}\n${endMarker}\n`;
|
|
427
|
+
writeFileSync(configPath, content + lineToAdd, 'utf-8');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
console.log(`Completion installed to ${configPath}`);
|
|
431
|
+
if (shell === 'powershell') {
|
|
432
|
+
console.log('Restart PowerShell to enable completion.');
|
|
433
|
+
} else {
|
|
434
|
+
console.log(`Run 'source ${configPath}' or restart your shell to enable completion.`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Handle --list-commands and --list-options before parsing.
|
|
440
|
+
*/
|
|
441
|
+
function handleCompletionOptions(): boolean {
|
|
442
|
+
const args = process.argv.slice(2);
|
|
443
|
+
|
|
444
|
+
if (args.includes('--list-commands')) {
|
|
445
|
+
const tools = loadToolsCache();
|
|
446
|
+
const allCommands = [...BUILTIN_COMMANDS, ...tools.tools.map((t) => t.name)];
|
|
447
|
+
console.log(allCommands.join('\n'));
|
|
448
|
+
return true;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const listOptionsIdx = args.indexOf('--list-options');
|
|
452
|
+
if (listOptionsIdx !== -1 && args[listOptionsIdx + 1]) {
|
|
453
|
+
const cmdName = args[listOptionsIdx + 1];
|
|
454
|
+
listOptionsForCommand(cmdName);
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* List options for a specific command.
|
|
463
|
+
*/
|
|
464
|
+
function listOptionsForCommand(cmdName: string): void {
|
|
465
|
+
// Built-in commands have no tool-specific options
|
|
466
|
+
if (BUILTIN_COMMANDS.includes(cmdName as (typeof BUILTIN_COMMANDS)[number])) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Tool commands - only output tool-specific options
|
|
471
|
+
const tools = loadToolsCache();
|
|
472
|
+
const tool = tools.tools.find((t) => t.name === cmdName);
|
|
473
|
+
if (!tool) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const options: string[] = [];
|
|
478
|
+
for (const propName of Object.keys(tool.inputSchema.properties)) {
|
|
479
|
+
const kebabName = pascalToKebabCase(propName);
|
|
480
|
+
options.push(`--${kebabName}`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
console.log(options.join('\n'));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Handle completion options first (before commander parsing)
|
|
487
|
+
if (!handleCompletionOptions()) {
|
|
488
|
+
program.parse();
|
|
489
|
+
}
|