uloop-cli 0.44.2 → 0.45.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/dist/cli.bundle.cjs +285 -92
- package/dist/cli.bundle.cjs.map +4 -4
- package/package.json +1 -1
- package/src/arg-parser.ts +3 -0
- package/src/cli.ts +70 -23
- package/src/default-tools.json +1 -1
- package/src/direct-unity-client.ts +3 -0
- package/src/execute-tool.ts +212 -50
- package/src/port-resolver.ts +3 -0
- package/src/project-root.ts +3 -0
- package/src/skills/skill-definitions/uloop-compile/SKILL.md +10 -0
- package/src/skills/skills-command.ts +3 -0
- package/src/skills/skills-manager.ts +3 -0
- package/src/spinner.ts +49 -0
- package/src/tool-cache.ts +3 -0
- package/src/version.ts +1 -1
package/package.json
CHANGED
package/src/arg-parser.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Converts CLI options to Unity tool parameters.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// Object keys come from tool schema definitions which are internal trusted data
|
|
7
|
+
/* eslint-disable security/detect-object-injection */
|
|
8
|
+
|
|
6
9
|
export interface ToolParameter {
|
|
7
10
|
Type: string;
|
|
8
11
|
Description: string;
|
package/src/cli.ts
CHANGED
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
* Commands are dynamically registered from tools.json cache.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// CLI tools output to console by design, file paths are constructed from trusted sources (project root detection),
|
|
8
|
+
// and object keys come from tool definitions which are internal trusted data
|
|
9
|
+
/* eslint-disable no-console, security/detect-non-literal-fs-filename, security/detect-object-injection */
|
|
10
|
+
|
|
11
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync } from 'fs';
|
|
8
12
|
import { join, basename, dirname } from 'path';
|
|
9
13
|
import { homedir } from 'os';
|
|
10
14
|
import { spawn } from 'child_process';
|
|
@@ -25,7 +29,7 @@ interface CliOptions extends GlobalOptions {
|
|
|
25
29
|
[key: string]: unknown;
|
|
26
30
|
}
|
|
27
31
|
|
|
28
|
-
const BUILTIN_COMMANDS = ['list', 'sync', 'completion', 'update', 'skills'] as const;
|
|
32
|
+
const BUILTIN_COMMANDS = ['list', 'sync', 'completion', 'update', 'fix', 'skills'] as const;
|
|
29
33
|
|
|
30
34
|
const program = new Command();
|
|
31
35
|
|
|
@@ -73,6 +77,13 @@ program
|
|
|
73
77
|
updateCli();
|
|
74
78
|
});
|
|
75
79
|
|
|
80
|
+
program
|
|
81
|
+
.command('fix')
|
|
82
|
+
.description('Clean up stale lock files that may prevent CLI from connecting')
|
|
83
|
+
.action(() => {
|
|
84
|
+
cleanupLockFiles();
|
|
85
|
+
});
|
|
86
|
+
|
|
76
87
|
// Register skills subcommand
|
|
77
88
|
registerSkillsCommand(program);
|
|
78
89
|
|
|
@@ -202,29 +213,39 @@ function extractGlobalOptions(options: Record<string, unknown>): GlobalOptions {
|
|
|
202
213
|
};
|
|
203
214
|
}
|
|
204
215
|
|
|
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
216
|
async function runWithErrorHandling(fn: () => Promise<void>): Promise<void> {
|
|
215
217
|
try {
|
|
216
218
|
await fn();
|
|
217
219
|
} catch (error) {
|
|
218
220
|
const message = error instanceof Error ? error.message : String(error);
|
|
219
221
|
|
|
222
|
+
if (message === 'UNITY_COMPILING') {
|
|
223
|
+
console.error('\x1b[33m⏳ Unity is compiling scripts.\x1b[0m');
|
|
224
|
+
console.error('Please wait for compilation to finish and try again.');
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (message === 'UNITY_DOMAIN_RELOAD') {
|
|
229
|
+
console.error('\x1b[33m⏳ Unity is reloading (Domain Reload in progress).\x1b[0m');
|
|
230
|
+
console.error('Please wait a moment and try again.');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (message === 'UNITY_SERVER_STARTING') {
|
|
235
|
+
console.error('\x1b[33m⏳ Unity server is starting.\x1b[0m');
|
|
236
|
+
console.error('Please wait a moment and try again.');
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (message === 'UNITY_NO_RESPONSE') {
|
|
241
|
+
console.error('\x1b[33m⏳ Unity is busy (no response received).\x1b[0m');
|
|
242
|
+
console.error('Unity may be compiling, reloading, or starting. Please wait and try again.');
|
|
243
|
+
process.exit(1);
|
|
244
|
+
}
|
|
245
|
+
|
|
220
246
|
if (message.includes('ECONNREFUSED')) {
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
}
|
|
247
|
+
console.error('\x1b[31mError: Cannot connect to Unity.\x1b[0m');
|
|
248
|
+
console.error('Make sure Unity is running with uLoopMCP installed.');
|
|
228
249
|
process.exit(1);
|
|
229
250
|
}
|
|
230
251
|
|
|
@@ -334,7 +355,6 @@ compdef _uloop uloop`;
|
|
|
334
355
|
* Update uloop CLI to the latest version using npm.
|
|
335
356
|
*/
|
|
336
357
|
function updateCli(): void {
|
|
337
|
-
// eslint-disable-next-line no-console
|
|
338
358
|
console.log('Updating uloop-cli to the latest version...');
|
|
339
359
|
|
|
340
360
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
@@ -345,24 +365,51 @@ function updateCli(): void {
|
|
|
345
365
|
|
|
346
366
|
child.on('close', (code) => {
|
|
347
367
|
if (code === 0) {
|
|
348
|
-
// eslint-disable-next-line no-console
|
|
349
368
|
console.log('\n✅ uloop-cli has been updated successfully!');
|
|
350
|
-
// eslint-disable-next-line no-console
|
|
351
369
|
console.log('Run "uloop --version" to check the new version.');
|
|
352
370
|
} else {
|
|
353
|
-
// eslint-disable-next-line no-console
|
|
354
371
|
console.error(`\n❌ Update failed with exit code ${code}`);
|
|
355
372
|
process.exit(1);
|
|
356
373
|
}
|
|
357
374
|
});
|
|
358
375
|
|
|
359
376
|
child.on('error', (err) => {
|
|
360
|
-
// eslint-disable-next-line no-console
|
|
361
377
|
console.error(`❌ Failed to run npm: ${err.message}`);
|
|
362
378
|
process.exit(1);
|
|
363
379
|
});
|
|
364
380
|
}
|
|
365
381
|
|
|
382
|
+
const LOCK_FILES = ['compiling.lock', 'domainreload.lock', 'serverstarting.lock'] as const;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Clean up stale lock files that may prevent CLI from connecting to Unity.
|
|
386
|
+
*/
|
|
387
|
+
function cleanupLockFiles(): void {
|
|
388
|
+
const projectRoot = findUnityProjectRoot();
|
|
389
|
+
if (projectRoot === null) {
|
|
390
|
+
console.error('Could not find Unity project root.');
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const tempDir = join(projectRoot, 'Temp');
|
|
395
|
+
let cleaned = 0;
|
|
396
|
+
|
|
397
|
+
for (const lockFile of LOCK_FILES) {
|
|
398
|
+
const lockPath = join(tempDir, lockFile);
|
|
399
|
+
if (existsSync(lockPath)) {
|
|
400
|
+
unlinkSync(lockPath);
|
|
401
|
+
console.log(`Removed: ${lockFile}`);
|
|
402
|
+
cleaned++;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (cleaned === 0) {
|
|
407
|
+
console.log('No lock files found.');
|
|
408
|
+
} else {
|
|
409
|
+
console.log(`\n✅ Cleaned up ${cleaned} lock file(s).`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
366
413
|
/**
|
|
367
414
|
* Handle completion command.
|
|
368
415
|
*/
|
package/src/default-tools.json
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Establishes one-shot TCP connections to Unity without going through MCP server.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// Non-null assertions are used after TCP frame parsing where data existence is guaranteed by protocol
|
|
7
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
8
|
+
|
|
6
9
|
import * as net from 'net';
|
|
7
10
|
import { createFrame, parseFrameFromBuffer, extractFrameFromBuffer } from './simple-framer.js';
|
|
8
11
|
|
package/src/execute-tool.ts
CHANGED
|
@@ -3,15 +3,99 @@
|
|
|
3
3
|
* Handles dynamic tool execution by connecting to Unity and sending requests.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// CLI tools output to console by design, object keys come from Unity tool responses which are trusted,
|
|
7
|
+
// and lock file paths are constructed from trusted project root detection
|
|
8
|
+
/* eslint-disable no-console, security/detect-object-injection, security/detect-non-literal-fs-filename */
|
|
9
|
+
|
|
10
|
+
import * as readline from 'readline';
|
|
11
|
+
import { existsSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
6
13
|
import { DirectUnityClient } from './direct-unity-client.js';
|
|
7
14
|
import { resolveUnityPort } from './port-resolver.js';
|
|
8
15
|
import { saveToolsCache, getCacheFilePath, ToolsCache, ToolDefinition } from './tool-cache.js';
|
|
9
16
|
import { VERSION } from './version.js';
|
|
17
|
+
import { createSpinner } from './spinner.js';
|
|
18
|
+
import { findUnityProjectRoot } from './project-root.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Suppress stdin echo during async operation to prevent escape sequences from being displayed.
|
|
22
|
+
* Returns a cleanup function to restore stdin state.
|
|
23
|
+
*/
|
|
24
|
+
function suppressStdinEcho(): () => void {
|
|
25
|
+
if (!process.stdin.isTTY) {
|
|
26
|
+
return () => {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const rl = readline.createInterface({
|
|
30
|
+
input: process.stdin,
|
|
31
|
+
output: process.stdout,
|
|
32
|
+
terminal: false,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
process.stdin.setRawMode(true);
|
|
36
|
+
process.stdin.resume();
|
|
37
|
+
|
|
38
|
+
const onData = (data: Buffer): void => {
|
|
39
|
+
// Ctrl+C (0x03) should trigger process exit
|
|
40
|
+
if (data[0] === 0x03) {
|
|
41
|
+
process.exit(130);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
process.stdin.on('data', onData);
|
|
45
|
+
|
|
46
|
+
return () => {
|
|
47
|
+
process.stdin.off('data', onData);
|
|
48
|
+
process.stdin.setRawMode(false);
|
|
49
|
+
process.stdin.pause();
|
|
50
|
+
rl.close();
|
|
51
|
+
};
|
|
52
|
+
}
|
|
10
53
|
|
|
11
54
|
export interface GlobalOptions {
|
|
12
55
|
port?: string;
|
|
13
56
|
}
|
|
14
57
|
|
|
58
|
+
const RETRY_DELAY_MS = 500;
|
|
59
|
+
const MAX_RETRIES = 3;
|
|
60
|
+
|
|
61
|
+
function sleep(ms: number): Promise<void> {
|
|
62
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isRetryableError(error: unknown): boolean {
|
|
66
|
+
if (!(error instanceof Error)) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
const message = error.message;
|
|
70
|
+
return message.includes('ECONNREFUSED') || message === 'UNITY_NO_RESPONSE';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if Unity is in a busy state (compiling, reloading, or server starting).
|
|
75
|
+
* Throws an error with appropriate message if busy.
|
|
76
|
+
*/
|
|
77
|
+
function checkUnityBusyState(): void {
|
|
78
|
+
const projectRoot = findUnityProjectRoot();
|
|
79
|
+
if (projectRoot === null) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const compilingLock = join(projectRoot, 'Temp', 'compiling.lock');
|
|
84
|
+
if (existsSync(compilingLock)) {
|
|
85
|
+
throw new Error('UNITY_COMPILING');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const domainReloadLock = join(projectRoot, 'Temp', 'domainreload.lock');
|
|
89
|
+
if (existsSync(domainReloadLock)) {
|
|
90
|
+
throw new Error('UNITY_DOMAIN_RELOAD');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const serverStartingLock = join(projectRoot, 'Temp', 'serverstarting.lock');
|
|
94
|
+
if (existsSync(serverStartingLock)) {
|
|
95
|
+
throw new Error('UNITY_SERVER_STARTING');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
15
99
|
export async function executeToolCommand(
|
|
16
100
|
toolName: string,
|
|
17
101
|
params: Record<string, unknown>,
|
|
@@ -27,18 +111,46 @@ export async function executeToolCommand(
|
|
|
27
111
|
}
|
|
28
112
|
const port = await resolveUnityPort(portNumber);
|
|
29
113
|
|
|
30
|
-
const
|
|
114
|
+
const restoreStdin = suppressStdinEcho();
|
|
115
|
+
const spinner = createSpinner('Connecting to Unity...');
|
|
116
|
+
|
|
117
|
+
let lastError: unknown;
|
|
118
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
119
|
+
checkUnityBusyState();
|
|
120
|
+
|
|
121
|
+
const client = new DirectUnityClient(port);
|
|
122
|
+
try {
|
|
123
|
+
await client.connect();
|
|
124
|
+
|
|
125
|
+
spinner.update(`Executing ${toolName}...`);
|
|
126
|
+
const result = await client.sendRequest(toolName, params);
|
|
31
127
|
|
|
32
|
-
|
|
33
|
-
|
|
128
|
+
if (result === undefined || result === null) {
|
|
129
|
+
throw new Error('UNITY_NO_RESPONSE');
|
|
130
|
+
}
|
|
34
131
|
|
|
35
|
-
|
|
132
|
+
// Success - stop spinner and output result
|
|
133
|
+
spinner.stop();
|
|
134
|
+
restoreStdin();
|
|
135
|
+
console.log(JSON.stringify(result, null, 2));
|
|
136
|
+
return;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
lastError = error;
|
|
139
|
+
client.disconnect();
|
|
36
140
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
141
|
+
if (!isRetryableError(error) || attempt >= MAX_RETRIES) {
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
spinner.update('Retrying connection...');
|
|
145
|
+
await sleep(RETRY_DELAY_MS);
|
|
146
|
+
} finally {
|
|
147
|
+
client.disconnect();
|
|
148
|
+
}
|
|
41
149
|
}
|
|
150
|
+
|
|
151
|
+
spinner.stop();
|
|
152
|
+
restoreStdin();
|
|
153
|
+
throw lastError;
|
|
42
154
|
}
|
|
43
155
|
|
|
44
156
|
export async function listAvailableTools(globalOptions: GlobalOptions): Promise<void> {
|
|
@@ -52,25 +164,50 @@ export async function listAvailableTools(globalOptions: GlobalOptions): Promise<
|
|
|
52
164
|
}
|
|
53
165
|
const port = await resolveUnityPort(portNumber);
|
|
54
166
|
|
|
55
|
-
const
|
|
167
|
+
const restoreStdin = suppressStdinEcho();
|
|
168
|
+
const spinner = createSpinner('Connecting to Unity...');
|
|
56
169
|
|
|
57
|
-
|
|
58
|
-
|
|
170
|
+
let lastError: unknown;
|
|
171
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
172
|
+
checkUnityBusyState();
|
|
59
173
|
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
174
|
+
const client = new DirectUnityClient(port);
|
|
175
|
+
try {
|
|
176
|
+
await client.connect();
|
|
63
177
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
178
|
+
spinner.update('Fetching tool list...');
|
|
179
|
+
const result = await client.sendRequest<{
|
|
180
|
+
Tools: Array<{ name: string; description: string }>;
|
|
181
|
+
}>('get-tool-details', { IncludeDevelopmentOnly: false });
|
|
67
182
|
|
|
68
|
-
|
|
69
|
-
|
|
183
|
+
if (!result.Tools || !Array.isArray(result.Tools)) {
|
|
184
|
+
throw new Error('Unexpected response from Unity: missing Tools array');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Success - stop spinner and output result
|
|
188
|
+
spinner.stop();
|
|
189
|
+
restoreStdin();
|
|
190
|
+
for (const tool of result.Tools) {
|
|
191
|
+
console.log(` - ${tool.name}`);
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
} catch (error) {
|
|
195
|
+
lastError = error;
|
|
196
|
+
client.disconnect();
|
|
197
|
+
|
|
198
|
+
if (!isRetryableError(error) || attempt >= MAX_RETRIES) {
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
spinner.update('Retrying connection...');
|
|
202
|
+
await sleep(RETRY_DELAY_MS);
|
|
203
|
+
} finally {
|
|
204
|
+
client.disconnect();
|
|
70
205
|
}
|
|
71
|
-
} finally {
|
|
72
|
-
client.disconnect();
|
|
73
206
|
}
|
|
207
|
+
|
|
208
|
+
spinner.stop();
|
|
209
|
+
restoreStdin();
|
|
210
|
+
throw lastError;
|
|
74
211
|
}
|
|
75
212
|
|
|
76
213
|
interface UnityToolInfo {
|
|
@@ -115,41 +252,66 @@ export async function syncTools(globalOptions: GlobalOptions): Promise<void> {
|
|
|
115
252
|
}
|
|
116
253
|
const port = await resolveUnityPort(portNumber);
|
|
117
254
|
|
|
118
|
-
const
|
|
255
|
+
const restoreStdin = suppressStdinEcho();
|
|
256
|
+
const spinner = createSpinner('Connecting to Unity...');
|
|
119
257
|
|
|
120
|
-
|
|
121
|
-
|
|
258
|
+
let lastError: unknown;
|
|
259
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
260
|
+
checkUnityBusyState();
|
|
122
261
|
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
262
|
+
const client = new DirectUnityClient(port);
|
|
263
|
+
try {
|
|
264
|
+
await client.connect();
|
|
126
265
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
266
|
+
spinner.update('Syncing tools...');
|
|
267
|
+
const result = await client.sendRequest<{
|
|
268
|
+
Tools: UnityToolInfo[];
|
|
269
|
+
}>('get-tool-details', { IncludeDevelopmentOnly: false });
|
|
130
270
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
271
|
+
spinner.stop();
|
|
272
|
+
if (!result.Tools || !Array.isArray(result.Tools)) {
|
|
273
|
+
restoreStdin();
|
|
274
|
+
throw new Error('Unexpected response from Unity: missing Tools array');
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const cache: ToolsCache = {
|
|
278
|
+
version: VERSION,
|
|
279
|
+
updatedAt: new Date().toISOString(),
|
|
280
|
+
tools: result.Tools.map((tool) => ({
|
|
281
|
+
name: tool.name,
|
|
282
|
+
description: tool.description,
|
|
283
|
+
inputSchema: {
|
|
284
|
+
type: 'object',
|
|
285
|
+
properties: convertProperties(tool.parameterSchema.Properties),
|
|
286
|
+
required: tool.parameterSchema.Required,
|
|
287
|
+
},
|
|
288
|
+
})),
|
|
289
|
+
};
|
|
144
290
|
|
|
145
|
-
|
|
291
|
+
saveToolsCache(cache);
|
|
146
292
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
293
|
+
console.log(`Synced ${cache.tools.length} tools to ${getCacheFilePath()}`);
|
|
294
|
+
console.log('\nTools:');
|
|
295
|
+
for (const tool of cache.tools) {
|
|
296
|
+
console.log(` - ${tool.name}`);
|
|
297
|
+
}
|
|
298
|
+
restoreStdin();
|
|
299
|
+
return;
|
|
300
|
+
} catch (error) {
|
|
301
|
+
lastError = error;
|
|
302
|
+
client.disconnect();
|
|
303
|
+
|
|
304
|
+
if (!isRetryableError(error) || attempt >= MAX_RETRIES) {
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
spinner.update('Retrying connection...');
|
|
308
|
+
await sleep(RETRY_DELAY_MS);
|
|
309
|
+
} finally {
|
|
310
|
+
client.disconnect();
|
|
151
311
|
}
|
|
152
|
-
} finally {
|
|
153
|
-
client.disconnect();
|
|
154
312
|
}
|
|
313
|
+
|
|
314
|
+
spinner.stop();
|
|
315
|
+
restoreStdin();
|
|
316
|
+
throw lastError;
|
|
155
317
|
}
|
package/src/port-resolver.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Resolves Unity server port from various sources.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// File paths are constructed from Unity project root detection, not from user input
|
|
7
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
8
|
+
|
|
6
9
|
import { readFile } from 'fs/promises';
|
|
7
10
|
import { join } from 'path';
|
|
8
11
|
import { findUnityProjectRoot } from './project-root.js';
|
package/src/project-root.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Searches upward from current directory to find Unity project markers.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
// Path traversal is intentional for finding Unity project root by walking up directory tree
|
|
7
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
8
|
+
|
|
6
9
|
import { existsSync } from 'fs';
|
|
7
10
|
import { join, dirname } from 'path';
|
|
8
11
|
|
|
@@ -35,3 +35,13 @@ Returns JSON:
|
|
|
35
35
|
- `Success`: boolean
|
|
36
36
|
- `ErrorCount`: number
|
|
37
37
|
- `WarningCount`: number
|
|
38
|
+
|
|
39
|
+
## Troubleshooting
|
|
40
|
+
|
|
41
|
+
If CLI hangs or shows "Unity is busy" errors after compilation, stale lock files may be preventing connection. Run the following to clean them up:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
uloop fix
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This removes any leftover lock files (`compiling.lock`, `domainreload.lock`, `serverstarting.lock`) from the Unity project's Temp directory.
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Skills manager for installing/uninstalling/listing uloop skills.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
// File paths are constructed from home directory and skill names, not from untrusted user input
|
|
6
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
7
|
+
|
|
5
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync } from 'fs';
|
|
6
9
|
import { join } from 'path';
|
|
7
10
|
import { homedir } from 'os';
|
package/src/spinner.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal spinner for showing loading state during async operations.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// Array index access is safe here (modulo operation keeps it in bounds)
|
|
6
|
+
/* eslint-disable security/detect-object-injection */
|
|
7
|
+
|
|
8
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] as const;
|
|
9
|
+
const FRAME_INTERVAL_MS = 80;
|
|
10
|
+
|
|
11
|
+
export interface Spinner {
|
|
12
|
+
update(message: string): void;
|
|
13
|
+
stop(): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Create a terminal spinner that displays a rotating animation with a message.
|
|
18
|
+
* Returns a Spinner object with update() and stop() methods.
|
|
19
|
+
*/
|
|
20
|
+
export function createSpinner(initialMessage: string): Spinner {
|
|
21
|
+
if (!process.stderr.isTTY) {
|
|
22
|
+
return {
|
|
23
|
+
update: (): void => {},
|
|
24
|
+
stop: (): void => {},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let frameIndex = 0;
|
|
29
|
+
let currentMessage = initialMessage;
|
|
30
|
+
|
|
31
|
+
const render = (): void => {
|
|
32
|
+
const frame = SPINNER_FRAMES[frameIndex];
|
|
33
|
+
process.stderr.write(`\r\x1b[K${frame} ${currentMessage}`);
|
|
34
|
+
frameIndex = (frameIndex + 1) % SPINNER_FRAMES.length;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
render();
|
|
38
|
+
const intervalId = setInterval(render, FRAME_INTERVAL_MS);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
update(message: string): void {
|
|
42
|
+
currentMessage = message;
|
|
43
|
+
},
|
|
44
|
+
stop(): void {
|
|
45
|
+
clearInterval(intervalId);
|
|
46
|
+
process.stderr.write('\r\x1b[K');
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
package/src/tool-cache.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
* Handles loading/saving tool definitions from .uloop/tools.json cache.
|
|
4
4
|
*/
|
|
5
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
|
+
|
|
6
9
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
7
10
|
import { join } from 'path';
|
|
8
11
|
import defaultToolsData from './default-tools.json';
|
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.45.0'; // x-release-please-version
|