uloop-cli 1.7.0 → 1.7.3
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 +352 -53
- package/dist/cli.bundle.cjs.map +4 -4
- package/package.json +6 -6
- package/src/__tests__/busy-state-order.test.ts +120 -0
- package/src/__tests__/cli-project-error.test.ts +45 -0
- package/src/__tests__/cli-update.test.ts +129 -0
- package/src/__tests__/execute-tool.test.ts +67 -2
- package/src/__tests__/package-metadata.test.ts +28 -0
- package/src/__tests__/port-resolver.test.ts +3 -5
- package/src/__tests__/unity-process.test.ts +289 -0
- package/src/cli-project-error.ts +36 -0
- package/src/cli.ts +19 -23
- package/src/default-tools.json +1 -1
- package/src/execute-tool.ts +82 -17
- package/src/port-resolver.ts +6 -6
- package/src/project-root.ts +1 -1
- package/src/unity-process.ts +337 -0
- package/src/version.ts +1 -1
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildUnityProcessCommand,
|
|
3
|
+
extractUnityProjectPath,
|
|
4
|
+
findRunningUnityProcessForProject,
|
|
5
|
+
isUnityEditorProcess,
|
|
6
|
+
isUnityProcessForProject,
|
|
7
|
+
normalizeUnityProjectPath,
|
|
8
|
+
parseUnityProcesses,
|
|
9
|
+
tokenizeCommandLine,
|
|
10
|
+
} from '../unity-process.js';
|
|
11
|
+
|
|
12
|
+
describe('buildUnityProcessCommand', () => {
|
|
13
|
+
it('builds ps command for macOS', () => {
|
|
14
|
+
expect(buildUnityProcessCommand('darwin')).toEqual({
|
|
15
|
+
command: 'ps',
|
|
16
|
+
args: ['-Ao', 'pid=,command='],
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('builds powershell command for Windows', () => {
|
|
21
|
+
expect(buildUnityProcessCommand('win32')).toEqual({
|
|
22
|
+
command: 'powershell.exe',
|
|
23
|
+
args: [
|
|
24
|
+
'-NoProfile',
|
|
25
|
+
'-NonInteractive',
|
|
26
|
+
'-Command',
|
|
27
|
+
'Get-CimInstance Win32_Process -Filter "name = \'Unity.exe\'" | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress',
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('tokenizeCommandLine', () => {
|
|
34
|
+
it('keeps quoted project path as one token', () => {
|
|
35
|
+
expect(
|
|
36
|
+
tokenizeCommandLine(
|
|
37
|
+
'/Applications/Unity.app/Contents/MacOS/Unity -projectPath "/Users/me/My Project"',
|
|
38
|
+
),
|
|
39
|
+
).toEqual([
|
|
40
|
+
'/Applications/Unity.app/Contents/MacOS/Unity',
|
|
41
|
+
'-projectPath',
|
|
42
|
+
'/Users/me/My Project',
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('extractUnityProjectPath', () => {
|
|
48
|
+
it('extracts macOS project path', () => {
|
|
49
|
+
expect(
|
|
50
|
+
extractUnityProjectPath(
|
|
51
|
+
'/Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/project',
|
|
52
|
+
),
|
|
53
|
+
).toBe('/Users/me/project');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('extracts Windows project path case-insensitively', () => {
|
|
57
|
+
expect(
|
|
58
|
+
extractUnityProjectPath(
|
|
59
|
+
'C:\\Program Files\\Unity\\Editor\\Unity.exe -projectpath "C:\\Work\\My Project"',
|
|
60
|
+
),
|
|
61
|
+
).toBe('C:\\Work\\My Project');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('normalizeUnityProjectPath', () => {
|
|
66
|
+
it('normalizes Windows paths case-insensitively', () => {
|
|
67
|
+
expect(normalizeUnityProjectPath('C:\\Work\\My Project\\', 'win32')).toBe('c:/work/my project');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('parseUnityProcesses', () => {
|
|
72
|
+
it('parses ps output', () => {
|
|
73
|
+
expect(
|
|
74
|
+
parseUnityProcesses(
|
|
75
|
+
'darwin',
|
|
76
|
+
'123 /Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/project\n',
|
|
77
|
+
),
|
|
78
|
+
).toEqual([
|
|
79
|
+
{
|
|
80
|
+
pid: 123,
|
|
81
|
+
commandLine: '/Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/project',
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('parses Windows powershell JSON array output', () => {
|
|
87
|
+
expect(
|
|
88
|
+
parseUnityProcesses(
|
|
89
|
+
'win32',
|
|
90
|
+
'[{"ProcessId":101,"CommandLine":"C:\\\\Program Files\\\\Unity\\\\Editor\\\\Unity.exe -projectPath \\"C:\\\\Work\\\\Project A\\""}]',
|
|
91
|
+
),
|
|
92
|
+
).toEqual([
|
|
93
|
+
{
|
|
94
|
+
pid: 101,
|
|
95
|
+
commandLine:
|
|
96
|
+
'C:\\Program Files\\Unity\\Editor\\Unity.exe -projectPath "C:\\Work\\Project A"',
|
|
97
|
+
},
|
|
98
|
+
]);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns empty array when Windows output is empty', () => {
|
|
102
|
+
expect(parseUnityProcesses('win32', '')).toEqual([]);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('isUnityProcessForProject', () => {
|
|
107
|
+
it('matches project path on macOS', () => {
|
|
108
|
+
expect(
|
|
109
|
+
isUnityProcessForProject(
|
|
110
|
+
'/Applications/Unity.app/Contents/MacOS/Unity -projectPath "/Users/me/Project A"',
|
|
111
|
+
'/Users/me/Project A',
|
|
112
|
+
'darwin',
|
|
113
|
+
),
|
|
114
|
+
).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('matches a macOS project path even when ps output has flattened quotes', () => {
|
|
118
|
+
expect(
|
|
119
|
+
isUnityProcessForProject(
|
|
120
|
+
'/Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/My Project',
|
|
121
|
+
'/Users/me/My Project',
|
|
122
|
+
'darwin',
|
|
123
|
+
),
|
|
124
|
+
).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('matches a macOS project path when ps output keeps trailing slashes', () => {
|
|
128
|
+
expect(
|
|
129
|
+
isUnityProcessForProject(
|
|
130
|
+
'/Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/My Project/',
|
|
131
|
+
'/Users/me/My Project',
|
|
132
|
+
'darwin',
|
|
133
|
+
),
|
|
134
|
+
).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('matches a macOS project path when ps output keeps repeated trailing slashes', () => {
|
|
138
|
+
expect(
|
|
139
|
+
isUnityProcessForProject(
|
|
140
|
+
'/Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/My Project///',
|
|
141
|
+
'/Users/me/My Project',
|
|
142
|
+
'darwin',
|
|
143
|
+
),
|
|
144
|
+
).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('does not match a different macOS project that only shares the prefix', () => {
|
|
148
|
+
expect(
|
|
149
|
+
isUnityProcessForProject(
|
|
150
|
+
'/Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/My Project Backup',
|
|
151
|
+
'/Users/me/My Project',
|
|
152
|
+
'darwin',
|
|
153
|
+
),
|
|
154
|
+
).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('matches project path on Windows case-insensitively', () => {
|
|
158
|
+
expect(
|
|
159
|
+
isUnityProcessForProject(
|
|
160
|
+
'C:\\Program Files\\Unity\\Editor\\Unity.exe -projectPath "C:\\Work\\Project A"',
|
|
161
|
+
'c:/work/project a',
|
|
162
|
+
'win32',
|
|
163
|
+
),
|
|
164
|
+
).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('isUnityEditorProcess', () => {
|
|
169
|
+
it('detects the Unity editor on macOS', () => {
|
|
170
|
+
expect(
|
|
171
|
+
isUnityEditorProcess(
|
|
172
|
+
'/Applications/Unity/Hub/Editor/2022.3.62f3/Unity.app/Contents/MacOS/Unity -projectPath "/Users/me/Project A"',
|
|
173
|
+
'darwin',
|
|
174
|
+
),
|
|
175
|
+
).toBe(true);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('rejects a non-Unity process on macOS even when projectPath is present', () => {
|
|
179
|
+
expect(
|
|
180
|
+
isUnityEditorProcess(
|
|
181
|
+
'/usr/local/bin/custom-tool -projectPath "/Users/me/Project A"',
|
|
182
|
+
'darwin',
|
|
183
|
+
),
|
|
184
|
+
).toBe(false);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('detects the Unity editor on Windows without relying on command line casing', () => {
|
|
188
|
+
expect(
|
|
189
|
+
isUnityEditorProcess(
|
|
190
|
+
'C:\\Program Files\\Unity\\Editor\\UNITY.EXE -projectPath "C:\\Work\\Project A"',
|
|
191
|
+
'win32',
|
|
192
|
+
),
|
|
193
|
+
).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('findRunningUnityProcessForProject', () => {
|
|
198
|
+
it('returns null when no Unity process is running', async () => {
|
|
199
|
+
const runCommand = jest.fn<Promise<string>, [string, string[]]>().mockResolvedValue('');
|
|
200
|
+
|
|
201
|
+
await expect(
|
|
202
|
+
findRunningUnityProcessForProject('/Users/me/project', {
|
|
203
|
+
platform: 'darwin',
|
|
204
|
+
runCommand,
|
|
205
|
+
}),
|
|
206
|
+
).resolves.toBeNull();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('returns matching Unity process on macOS', async () => {
|
|
210
|
+
const runCommand = jest
|
|
211
|
+
.fn<Promise<string>, [string, string[]]>()
|
|
212
|
+
.mockResolvedValue(
|
|
213
|
+
[
|
|
214
|
+
'111 /Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/other',
|
|
215
|
+
'222 /Applications/Unity.app/Contents/MacOS/Unity -projectPath "/Users/me/project"',
|
|
216
|
+
].join('\n'),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
await expect(
|
|
220
|
+
findRunningUnityProcessForProject('/Users/me/project', {
|
|
221
|
+
platform: 'darwin',
|
|
222
|
+
runCommand,
|
|
223
|
+
}),
|
|
224
|
+
).resolves.toEqual({ pid: 222 });
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('returns a matching macOS Unity process when ps output has flattened quotes', async () => {
|
|
228
|
+
const runCommand = jest
|
|
229
|
+
.fn<Promise<string>, [string, string[]]>()
|
|
230
|
+
.mockResolvedValue(
|
|
231
|
+
'222 /Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/My Project',
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
await expect(
|
|
235
|
+
findRunningUnityProcessForProject('/Users/me/My Project', {
|
|
236
|
+
platform: 'darwin',
|
|
237
|
+
runCommand,
|
|
238
|
+
}),
|
|
239
|
+
).resolves.toEqual({ pid: 222 });
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('returns a matching macOS Unity process when ps output keeps trailing slashes', async () => {
|
|
243
|
+
const runCommand = jest
|
|
244
|
+
.fn<Promise<string>, [string, string[]]>()
|
|
245
|
+
.mockResolvedValue(
|
|
246
|
+
'222 /Applications/Unity.app/Contents/MacOS/Unity -projectPath /Users/me/My Project///',
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
await expect(
|
|
250
|
+
findRunningUnityProcessForProject('/Users/me/My Project', {
|
|
251
|
+
platform: 'darwin',
|
|
252
|
+
runCommand,
|
|
253
|
+
}),
|
|
254
|
+
).resolves.toEqual({ pid: 222 });
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('ignores non-Unity processes that happen to share the same projectPath', async () => {
|
|
258
|
+
const runCommand = jest
|
|
259
|
+
.fn<Promise<string>, [string, string[]]>()
|
|
260
|
+
.mockResolvedValue(
|
|
261
|
+
[
|
|
262
|
+
'111 /usr/local/bin/custom-tool -projectPath "/Users/me/project"',
|
|
263
|
+
'222 /Applications/Unity.app/Contents/MacOS/Unity -projectPath "/Users/me/project"',
|
|
264
|
+
].join('\n'),
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await expect(
|
|
268
|
+
findRunningUnityProcessForProject('/Users/me/project', {
|
|
269
|
+
platform: 'darwin',
|
|
270
|
+
runCommand,
|
|
271
|
+
}),
|
|
272
|
+
).resolves.toEqual({ pid: 222 });
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('returns matching Unity process on Windows', async () => {
|
|
276
|
+
const runCommand = jest
|
|
277
|
+
.fn<Promise<string>, [string, string[]]>()
|
|
278
|
+
.mockResolvedValue(
|
|
279
|
+
'[{"ProcessId":333,"CommandLine":"C:\\\\Program Files\\\\Unity\\\\Editor\\\\UNITY.EXE -projectPath \\"C:\\\\Work\\\\My Project\\""}]',
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
await expect(
|
|
283
|
+
findRunningUnityProcessForProject('c:/work/my project', {
|
|
284
|
+
platform: 'win32',
|
|
285
|
+
runCommand,
|
|
286
|
+
}),
|
|
287
|
+
).resolves.toEqual({ pid: 333 });
|
|
288
|
+
});
|
|
289
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { UnityNotRunningError, UnityServerNotRunningError } from './port-resolver.js';
|
|
2
|
+
import { ProjectMismatchError } from './project-validator.js';
|
|
3
|
+
|
|
4
|
+
export function getProjectResolutionErrorLines(
|
|
5
|
+
error: UnityNotRunningError | UnityServerNotRunningError | ProjectMismatchError,
|
|
6
|
+
): string[] {
|
|
7
|
+
if (error instanceof UnityServerNotRunningError) {
|
|
8
|
+
return [
|
|
9
|
+
'Error: Unity Editor is running, but Unity CLI Loop server is not.',
|
|
10
|
+
'',
|
|
11
|
+
` Project: ${error.projectRoot}`,
|
|
12
|
+
'',
|
|
13
|
+
'Start the server from: Window > Unity CLI Loop > Server',
|
|
14
|
+
];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (error instanceof UnityNotRunningError) {
|
|
18
|
+
return [
|
|
19
|
+
'Error: Unity Editor for this project is not running.',
|
|
20
|
+
'',
|
|
21
|
+
` Project: ${error.projectRoot}`,
|
|
22
|
+
'',
|
|
23
|
+
'Start the Unity Editor for this project and try again.',
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return [
|
|
28
|
+
'Error: Connected Unity instance belongs to a different project.',
|
|
29
|
+
'',
|
|
30
|
+
` Project: ${error.expectedProjectRoot}`,
|
|
31
|
+
` Connected to: ${error.connectedProjectRoot}`,
|
|
32
|
+
'',
|
|
33
|
+
'Another Unity instance was found, but it belongs to a different project.',
|
|
34
|
+
'Start the Unity Editor for this project, or use --project-path to specify the target.',
|
|
35
|
+
];
|
|
36
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -36,9 +36,14 @@ import { registerLaunchCommand } from './commands/launch.js';
|
|
|
36
36
|
import { registerFocusWindowCommand } from './commands/focus-window.js';
|
|
37
37
|
import { VERSION } from './version.js';
|
|
38
38
|
import { findUnityProjectRoot } from './project-root.js';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
validateProjectPath,
|
|
41
|
+
UnityNotRunningError,
|
|
42
|
+
UnityServerNotRunningError,
|
|
43
|
+
} from './port-resolver.js';
|
|
40
44
|
import { ProjectMismatchError } from './project-validator.js';
|
|
41
45
|
import { filterEnabledTools, isToolEnabled } from './tool-settings-loader.js';
|
|
46
|
+
import { getProjectResolutionErrorLines } from './cli-project-error.js';
|
|
42
47
|
|
|
43
48
|
interface CliOptions extends GlobalOptions {
|
|
44
49
|
[key: string]: unknown;
|
|
@@ -429,25 +434,17 @@ async function runWithErrorHandling(fn: () => Promise<void>): Promise<void> {
|
|
|
429
434
|
try {
|
|
430
435
|
await fn();
|
|
431
436
|
} catch (error) {
|
|
432
|
-
if (error instanceof UnityNotRunningError) {
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
console.error('');
|
|
437
|
-
console.error('Start the Unity Editor for this project and try again.');
|
|
437
|
+
if (error instanceof UnityNotRunningError || error instanceof UnityServerNotRunningError) {
|
|
438
|
+
for (const line of getProjectResolutionErrorLines(error)) {
|
|
439
|
+
console.error(line.startsWith('Error: ') ? `\x1b[31m${line}\x1b[0m` : line);
|
|
440
|
+
}
|
|
438
441
|
process.exit(1);
|
|
439
442
|
}
|
|
440
443
|
|
|
441
444
|
if (error instanceof ProjectMismatchError) {
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
console.error(` Connected to: ${error.connectedProjectRoot}`);
|
|
446
|
-
console.error('');
|
|
447
|
-
console.error('Another Unity instance was found, but it belongs to a different project.');
|
|
448
|
-
console.error(
|
|
449
|
-
'Start the Unity Editor for this project, or use --project-path to specify the target.',
|
|
450
|
-
);
|
|
445
|
+
for (const line of getProjectResolutionErrorLines(error)) {
|
|
446
|
+
console.error(line.startsWith('Error: ') ? `\x1b[31m${line}\x1b[0m` : line);
|
|
447
|
+
}
|
|
451
448
|
process.exit(1);
|
|
452
449
|
}
|
|
453
450
|
|
|
@@ -604,11 +601,9 @@ compdef _uloop uloop`;
|
|
|
604
601
|
/**
|
|
605
602
|
* Get the currently installed version of uloop-cli from npm.
|
|
606
603
|
*/
|
|
607
|
-
function getInstalledVersion(callback: (version: string | null) => void): void {
|
|
604
|
+
export function getInstalledVersion(callback: (version: string | null) => void): void {
|
|
608
605
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
609
|
-
const child = spawn(npmCommand, ['list', '-g', 'uloop-cli', '--json']
|
|
610
|
-
shell: true,
|
|
611
|
-
});
|
|
606
|
+
const child = spawn(npmCommand, ['list', '-g', 'uloop-cli', '--json']);
|
|
612
607
|
|
|
613
608
|
let stdout = '';
|
|
614
609
|
child.stdout.on('data', (data: Buffer) => {
|
|
@@ -663,14 +658,13 @@ function getInstalledVersion(callback: (version: string | null) => void): void {
|
|
|
663
658
|
/**
|
|
664
659
|
* Update uloop CLI to the latest version using npm.
|
|
665
660
|
*/
|
|
666
|
-
function updateCli(): void {
|
|
661
|
+
export function updateCli(): void {
|
|
667
662
|
const previousVersion = VERSION;
|
|
668
663
|
console.log('Updating uloop-cli to the latest version...');
|
|
669
664
|
|
|
670
665
|
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
671
666
|
const child = spawn(npmCommand, ['install', '-g', 'uloop-cli@latest'], {
|
|
672
667
|
stdio: 'inherit',
|
|
673
|
-
shell: true,
|
|
674
668
|
});
|
|
675
669
|
|
|
676
670
|
child.on('close', (code) => {
|
|
@@ -1039,4 +1033,6 @@ async function main(): Promise<void> {
|
|
|
1039
1033
|
program.parse();
|
|
1040
1034
|
}
|
|
1041
1035
|
|
|
1042
|
-
|
|
1036
|
+
if (process.env.JEST_WORKER_ID === undefined) {
|
|
1037
|
+
void main();
|
|
1038
|
+
}
|
package/src/default-tools.json
CHANGED
package/src/execute-tool.ts
CHANGED
|
@@ -13,12 +13,18 @@ import { existsSync } from 'fs';
|
|
|
13
13
|
import { join } from 'path';
|
|
14
14
|
import * as semver from 'semver';
|
|
15
15
|
import { DirectUnityClient } from './direct-unity-client.js';
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
resolveUnityPort,
|
|
18
|
+
UnityNotRunningError,
|
|
19
|
+
UnityServerNotRunningError,
|
|
20
|
+
validateProjectPath,
|
|
21
|
+
} from './port-resolver.js';
|
|
17
22
|
import { validateConnectedProject } from './project-validator.js';
|
|
18
23
|
import { saveToolsCache, getCacheFilePath, ToolsCache, ToolDefinition } from './tool-cache.js';
|
|
19
24
|
import { VERSION } from './version.js';
|
|
20
25
|
import { createSpinner } from './spinner.js';
|
|
21
26
|
import { findUnityProjectRoot } from './project-root.js';
|
|
27
|
+
import { findRunningUnityProcessForProject } from './unity-process.js';
|
|
22
28
|
import {
|
|
23
29
|
type CompileExecutionOptions,
|
|
24
30
|
ensureCompileRequestId,
|
|
@@ -77,6 +83,14 @@ const MAX_RETRIES = 3;
|
|
|
77
83
|
const COMPILE_WAIT_TIMEOUT_MS = 90000;
|
|
78
84
|
const COMPILE_WAIT_POLL_INTERVAL_MS = 100;
|
|
79
85
|
|
|
86
|
+
interface ConnectionFailureDiagnosisDependencies {
|
|
87
|
+
findRunningUnityProcessForProjectFn: typeof findRunningUnityProcessForProject;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const defaultConnectionFailureDiagnosisDependencies: ConnectionFailureDiagnosisDependencies = {
|
|
91
|
+
findRunningUnityProcessForProjectFn: findRunningUnityProcessForProject,
|
|
92
|
+
};
|
|
93
|
+
|
|
80
94
|
function getCompileExecutionOptions(
|
|
81
95
|
toolName: string,
|
|
82
96
|
params: Record<string, unknown>,
|
|
@@ -103,6 +117,54 @@ function isRetryableError(error: unknown): boolean {
|
|
|
103
117
|
);
|
|
104
118
|
}
|
|
105
119
|
|
|
120
|
+
export async function diagnoseRetryableProjectConnectionError(
|
|
121
|
+
error: unknown,
|
|
122
|
+
projectRoot: string | null,
|
|
123
|
+
shouldDiagnoseProjectState: boolean,
|
|
124
|
+
dependencies: ConnectionFailureDiagnosisDependencies = defaultConnectionFailureDiagnosisDependencies,
|
|
125
|
+
): Promise<unknown> {
|
|
126
|
+
if (!shouldDiagnoseProjectState || projectRoot === null || !isRetryableError(error)) {
|
|
127
|
+
return error;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const runningProcess = await dependencies
|
|
131
|
+
.findRunningUnityProcessForProjectFn(projectRoot)
|
|
132
|
+
.catch(() => undefined);
|
|
133
|
+
|
|
134
|
+
if (runningProcess === undefined) {
|
|
135
|
+
return error;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (runningProcess === null) {
|
|
139
|
+
return new UnityNotRunningError(projectRoot);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return new UnityServerNotRunningError(projectRoot);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function throwFinalToolError(
|
|
146
|
+
error: unknown,
|
|
147
|
+
projectRoot: string | null,
|
|
148
|
+
shouldDiagnoseProjectState: boolean,
|
|
149
|
+
): Promise<never> {
|
|
150
|
+
const diagnosedError = await diagnoseRetryableProjectConnectionError(
|
|
151
|
+
error,
|
|
152
|
+
projectRoot,
|
|
153
|
+
shouldDiagnoseProjectState,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (diagnosedError instanceof Error) {
|
|
157
|
+
throw diagnosedError;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (typeof diagnosedError === 'string') {
|
|
161
|
+
throw new Error(diagnosedError);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const serializedError = JSON.stringify(diagnosedError);
|
|
165
|
+
throw new Error(serializedError ?? 'Unknown error');
|
|
166
|
+
}
|
|
167
|
+
|
|
106
168
|
// Distinct from isRetryableError(): that function covers pre-connection failures
|
|
107
169
|
// (ECONNREFUSED, EADDRNOTAVAIL) which cannot occur after dispatch.
|
|
108
170
|
// This function covers post-dispatch TCP failures where Unity may have received
|
|
@@ -188,6 +250,14 @@ function checkUnityBusyState(projectPath?: string): void {
|
|
|
188
250
|
}
|
|
189
251
|
}
|
|
190
252
|
|
|
253
|
+
function checkUnityBusyStateBeforeProjectResolution(globalOptions: GlobalOptions): void {
|
|
254
|
+
if (globalOptions.port !== undefined) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
checkUnityBusyState(globalOptions.projectPath);
|
|
259
|
+
}
|
|
260
|
+
|
|
191
261
|
export async function executeToolCommand(
|
|
192
262
|
toolName: string,
|
|
193
263
|
params: Record<string, unknown>,
|
|
@@ -201,6 +271,7 @@ export async function executeToolCommand(
|
|
|
201
271
|
}
|
|
202
272
|
portNumber = parsed;
|
|
203
273
|
}
|
|
274
|
+
checkUnityBusyStateBeforeProjectResolution(globalOptions);
|
|
204
275
|
const port = await resolveUnityPort(portNumber, globalOptions.projectPath);
|
|
205
276
|
const compileOptions = getCompileExecutionOptions(toolName, params);
|
|
206
277
|
const shouldWaitForDomainReload = compileOptions.waitForDomainReload;
|
|
@@ -228,7 +299,7 @@ export async function executeToolCommand(
|
|
|
228
299
|
let requestDispatched = false;
|
|
229
300
|
|
|
230
301
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
231
|
-
|
|
302
|
+
checkUnityBusyStateBeforeProjectResolution(globalOptions);
|
|
232
303
|
|
|
233
304
|
const client = new DirectUnityClient(port);
|
|
234
305
|
try {
|
|
@@ -308,8 +379,8 @@ export async function executeToolCommand(
|
|
|
308
379
|
if (immediateResult === undefined && !requestDispatched) {
|
|
309
380
|
spinner.stop();
|
|
310
381
|
restoreStdin();
|
|
311
|
-
if (lastError
|
|
312
|
-
|
|
382
|
+
if (lastError !== undefined) {
|
|
383
|
+
await throwFinalToolError(lastError, projectRoot, shouldValidateProject);
|
|
313
384
|
}
|
|
314
385
|
throw new Error(
|
|
315
386
|
'Compile request never reached Unity. Check that Unity is running and retry.',
|
|
@@ -371,15 +442,7 @@ export async function executeToolCommand(
|
|
|
371
442
|
if (lastError === undefined) {
|
|
372
443
|
throw new Error('Tool execution failed without error details.');
|
|
373
444
|
}
|
|
374
|
-
|
|
375
|
-
throw lastError;
|
|
376
|
-
}
|
|
377
|
-
if (typeof lastError === 'string') {
|
|
378
|
-
throw new Error(lastError);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const serializedError = JSON.stringify(lastError);
|
|
382
|
-
throw new Error(serializedError ?? 'Unknown error');
|
|
445
|
+
await throwFinalToolError(lastError, projectRoot, shouldValidateProject);
|
|
383
446
|
}
|
|
384
447
|
|
|
385
448
|
export async function listAvailableTools(globalOptions: GlobalOptions): Promise<void> {
|
|
@@ -391,6 +454,7 @@ export async function listAvailableTools(globalOptions: GlobalOptions): Promise<
|
|
|
391
454
|
}
|
|
392
455
|
portNumber = parsed;
|
|
393
456
|
}
|
|
457
|
+
checkUnityBusyStateBeforeProjectResolution(globalOptions);
|
|
394
458
|
const port = await resolveUnityPort(portNumber, globalOptions.projectPath);
|
|
395
459
|
const projectRoot =
|
|
396
460
|
globalOptions.projectPath !== undefined
|
|
@@ -403,7 +467,7 @@ export async function listAvailableTools(globalOptions: GlobalOptions): Promise<
|
|
|
403
467
|
|
|
404
468
|
let lastError: unknown;
|
|
405
469
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
406
|
-
|
|
470
|
+
checkUnityBusyStateBeforeProjectResolution(globalOptions);
|
|
407
471
|
|
|
408
472
|
const client = new DirectUnityClient(port);
|
|
409
473
|
try {
|
|
@@ -445,7 +509,7 @@ export async function listAvailableTools(globalOptions: GlobalOptions): Promise<
|
|
|
445
509
|
|
|
446
510
|
spinner.stop();
|
|
447
511
|
restoreStdin();
|
|
448
|
-
|
|
512
|
+
await throwFinalToolError(lastError, projectRoot, shouldValidateProject);
|
|
449
513
|
}
|
|
450
514
|
|
|
451
515
|
interface UnityToolInfo {
|
|
@@ -488,6 +552,7 @@ export async function syncTools(globalOptions: GlobalOptions): Promise<void> {
|
|
|
488
552
|
}
|
|
489
553
|
portNumber = parsed;
|
|
490
554
|
}
|
|
555
|
+
checkUnityBusyStateBeforeProjectResolution(globalOptions);
|
|
491
556
|
const port = await resolveUnityPort(portNumber, globalOptions.projectPath);
|
|
492
557
|
const projectRoot =
|
|
493
558
|
globalOptions.projectPath !== undefined
|
|
@@ -500,7 +565,7 @@ export async function syncTools(globalOptions: GlobalOptions): Promise<void> {
|
|
|
500
565
|
|
|
501
566
|
let lastError: unknown;
|
|
502
567
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
503
|
-
|
|
568
|
+
checkUnityBusyStateBeforeProjectResolution(globalOptions);
|
|
504
569
|
|
|
505
570
|
const client = new DirectUnityClient(port);
|
|
506
571
|
try {
|
|
@@ -562,5 +627,5 @@ export async function syncTools(globalOptions: GlobalOptions): Promise<void> {
|
|
|
562
627
|
|
|
563
628
|
spinner.stop();
|
|
564
629
|
restoreStdin();
|
|
565
|
-
|
|
630
|
+
await throwFinalToolError(lastError, projectRoot, shouldValidateProject);
|
|
566
631
|
}
|
package/src/port-resolver.ts
CHANGED
|
@@ -23,6 +23,12 @@ export class UnityNotRunningError extends Error {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export class UnityServerNotRunningError extends Error {
|
|
27
|
+
constructor(public readonly projectRoot: string) {
|
|
28
|
+
super('UNITY_SERVER_NOT_RUNNING');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
interface UnityMcpSettings {
|
|
27
33
|
isServerRunning?: boolean;
|
|
28
34
|
customPort?: number;
|
|
@@ -130,12 +136,6 @@ async function readPortFromSettingsOrThrow(projectRoot: string): Promise<number>
|
|
|
130
136
|
}
|
|
131
137
|
const settings = parsed as UnityMcpSettings;
|
|
132
138
|
|
|
133
|
-
// Only block when isServerRunning is explicitly false (Unity clean shutdown).
|
|
134
|
-
// undefined/missing means old settings format — proceed to next validation stage.
|
|
135
|
-
if (settings.isServerRunning === false) {
|
|
136
|
-
throw new UnityNotRunningError(projectRoot);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
139
|
const port = resolvePortFromUnitySettings(settings);
|
|
140
140
|
if (port !== null) {
|
|
141
141
|
return port;
|
package/src/project-root.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function getUnitySettingsCandidatePaths(dirPath: string): string[] {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function hasUloopInstalled(dirPath: string): boolean {
|
|
37
|
-
return getUnitySettingsCandidatePaths(dirPath).some(path => existsSync(path));
|
|
37
|
+
return getUnitySettingsCandidatePaths(dirPath).some((path) => existsSync(path));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function isUnityProjectWithUloop(dirPath: string): boolean {
|