uloop-cli 1.7.2 → 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 +289 -30
- package/dist/cli.bundle.cjs.map +4 -4
- package/package.json +4 -4
- package/src/__tests__/cli-project-error.test.ts +13 -1
- 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 +12 -2
- package/src/cli.ts +6 -2
- package/src/default-tools.json +1 -1
- package/src/execute-tool.ts +68 -14
- package/src/port-resolver.ts +6 -6
- package/src/unity-process.ts +337 -0
- package/src/version.ts +1 -1
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;
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
const WINDOWS_PROCESS_QUERY =
|
|
6
|
+
'Get-CimInstance Win32_Process -Filter "name = \'Unity.exe\'" | Select-Object ProcessId, CommandLine | ConvertTo-Json -Compress';
|
|
7
|
+
|
|
8
|
+
interface RunningUnityProcess {
|
|
9
|
+
pid: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface RawUnityProcess {
|
|
13
|
+
pid: number;
|
|
14
|
+
commandLine: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface UnityProcessCommand {
|
|
18
|
+
command: string;
|
|
19
|
+
args: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface UnityProcessDependencies {
|
|
23
|
+
platform: NodeJS.Platform;
|
|
24
|
+
runCommand: (command: string, args: string[]) => Promise<string>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const defaultDependencies: UnityProcessDependencies = {
|
|
28
|
+
platform: process.platform,
|
|
29
|
+
runCommand: runUnityProcessQuery,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function buildUnityProcessCommand(platform: NodeJS.Platform): UnityProcessCommand | null {
|
|
33
|
+
if (platform === 'darwin') {
|
|
34
|
+
return {
|
|
35
|
+
command: 'ps',
|
|
36
|
+
args: ['-Ao', 'pid=,command='],
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (platform === 'linux') {
|
|
41
|
+
return {
|
|
42
|
+
command: 'ps',
|
|
43
|
+
args: ['-eo', 'pid=,args='],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (platform === 'win32') {
|
|
48
|
+
return {
|
|
49
|
+
command: 'powershell.exe',
|
|
50
|
+
args: ['-NoProfile', '-NonInteractive', '-Command', WINDOWS_PROCESS_QUERY],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseUnityProcesses(platform: NodeJS.Platform, output: string): RawUnityProcess[] {
|
|
58
|
+
if (platform === 'win32') {
|
|
59
|
+
return parseWindowsUnityProcesses(output);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return parsePsUnityProcesses(output);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function tokenizeCommandLine(commandLine: string): string[] {
|
|
66
|
+
const tokens: string[] = [];
|
|
67
|
+
let current = '';
|
|
68
|
+
let inQuotes = false;
|
|
69
|
+
|
|
70
|
+
for (let i = 0; i < commandLine.length; i++) {
|
|
71
|
+
const character = commandLine[i];
|
|
72
|
+
|
|
73
|
+
if (character === '"') {
|
|
74
|
+
inQuotes = !inQuotes;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!inQuotes && /\s/.test(character)) {
|
|
79
|
+
if (current.length > 0) {
|
|
80
|
+
tokens.push(current);
|
|
81
|
+
current = '';
|
|
82
|
+
}
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
current += character;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (current.length > 0) {
|
|
90
|
+
tokens.push(current);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return tokens;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function extractUnityProjectPath(commandLine: string): string | null {
|
|
97
|
+
const tokens = tokenizeCommandLine(commandLine);
|
|
98
|
+
|
|
99
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
100
|
+
const token = tokens[i].toLowerCase();
|
|
101
|
+
if (token !== '-projectpath') {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const projectPath = tokens[i + 1];
|
|
106
|
+
return projectPath ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function normalizeUnityProjectPath(projectPath: string, platform: NodeJS.Platform): string {
|
|
113
|
+
const normalizedSeparators = projectPath.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
114
|
+
if (platform === 'win32') {
|
|
115
|
+
return normalizedSeparators.toLowerCase();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return normalizedSeparators;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isUnityProcessForProject(
|
|
122
|
+
commandLine: string,
|
|
123
|
+
projectRoot: string,
|
|
124
|
+
platform: NodeJS.Platform,
|
|
125
|
+
): boolean {
|
|
126
|
+
if (platform !== 'win32') {
|
|
127
|
+
return commandLineContainsProjectRoot(commandLine, projectRoot, platform);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const extractedProjectPath = extractUnityProjectPath(commandLine);
|
|
131
|
+
if (extractedProjectPath === null) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
normalizeUnityProjectPath(extractedProjectPath, platform) ===
|
|
137
|
+
normalizeUnityProjectPath(projectRoot, platform)
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function commandLineContainsProjectRoot(
|
|
142
|
+
commandLine: string,
|
|
143
|
+
projectRoot: string,
|
|
144
|
+
platform: NodeJS.Platform,
|
|
145
|
+
): boolean {
|
|
146
|
+
const projectPathFlagIndex = commandLine.toLowerCase().indexOf(' -projectpath');
|
|
147
|
+
if (projectPathFlagIndex === -1) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const normalizedProjectRoot = normalizeUnityProjectPath(projectRoot, platform);
|
|
152
|
+
let projectRootIndex = commandLine.indexOf(normalizedProjectRoot, projectPathFlagIndex);
|
|
153
|
+
|
|
154
|
+
while (projectRootIndex !== -1) {
|
|
155
|
+
const beforeProjectRoot = commandLine[projectRootIndex - 1];
|
|
156
|
+
const projectPathEndIndex = skipTrailingProjectPathSeparators(
|
|
157
|
+
commandLine,
|
|
158
|
+
projectRootIndex + normalizedProjectRoot.length,
|
|
159
|
+
);
|
|
160
|
+
if (
|
|
161
|
+
isProjectPathBoundaryCharacter(beforeProjectRoot) &&
|
|
162
|
+
isProjectPathTerminator(commandLine, projectPathEndIndex)
|
|
163
|
+
) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
projectRootIndex = commandLine.indexOf(normalizedProjectRoot, projectRootIndex + 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isProjectPathBoundaryCharacter(character: string | undefined): boolean {
|
|
174
|
+
return character === undefined || /\s|["']/.test(character);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function skipTrailingProjectPathSeparators(commandLine: string, startIndex: number): number {
|
|
178
|
+
let index = startIndex;
|
|
179
|
+
|
|
180
|
+
while (readCharacterAt(commandLine, index) === '/') {
|
|
181
|
+
index += 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return index;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function isProjectPathTerminator(commandLine: string, projectRootEndIndex: number): boolean {
|
|
188
|
+
const character = readCharacterAt(commandLine, projectRootEndIndex);
|
|
189
|
+
if (character === null) {
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (character === '"' || character === "'") {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!/\s/.test(character)) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (let i = projectRootEndIndex; i < commandLine.length; i++) {
|
|
202
|
+
const trailingCharacter = readCharacterAt(commandLine, i);
|
|
203
|
+
if (trailingCharacter === null) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (/\s/.test(trailingCharacter)) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return trailingCharacter === '-';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readCharacterAt(value: string, index: number): string | null {
|
|
218
|
+
const character = value.slice(index, index + 1);
|
|
219
|
+
if (character.length === 0) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return character;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function isUnityEditorProcess(commandLine: string, platform: NodeJS.Platform): boolean {
|
|
227
|
+
const lowerCommandLine = commandLine.toLowerCase();
|
|
228
|
+
if (lowerCommandLine.length === 0) {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const projectPathFlagIndex = lowerCommandLine.indexOf(' -projectpath');
|
|
233
|
+
const executableSection =
|
|
234
|
+
projectPathFlagIndex === -1
|
|
235
|
+
? lowerCommandLine
|
|
236
|
+
: lowerCommandLine.slice(0, projectPathFlagIndex);
|
|
237
|
+
|
|
238
|
+
if (platform === 'win32') {
|
|
239
|
+
return executableSection.includes('unity.exe');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (platform === 'darwin') {
|
|
243
|
+
return executableSection.includes('/unity.app/contents/macos/unity');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (platform === 'linux') {
|
|
247
|
+
return (
|
|
248
|
+
executableSection.endsWith('/unity') ||
|
|
249
|
+
executableSection.endsWith('/unity-editor') ||
|
|
250
|
+
executableSection.includes('/editor/unity')
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function findRunningUnityProcessForProject(
|
|
258
|
+
projectRoot: string,
|
|
259
|
+
dependencies: UnityProcessDependencies = defaultDependencies,
|
|
260
|
+
): Promise<RunningUnityProcess | null> {
|
|
261
|
+
const unityProcessCommand = buildUnityProcessCommand(dependencies.platform);
|
|
262
|
+
if (unityProcessCommand === null) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const output = await dependencies.runCommand(
|
|
267
|
+
unityProcessCommand.command,
|
|
268
|
+
unityProcessCommand.args,
|
|
269
|
+
);
|
|
270
|
+
const runningProcesses = parseUnityProcesses(dependencies.platform, output);
|
|
271
|
+
const matchingProcess = runningProcesses.find(
|
|
272
|
+
(processInfo) =>
|
|
273
|
+
isUnityEditorProcess(processInfo.commandLine, dependencies.platform) &&
|
|
274
|
+
isUnityProcessForProject(processInfo.commandLine, projectRoot, dependencies.platform),
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
if (matchingProcess === undefined) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
pid: matchingProcess.pid,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function runUnityProcessQuery(command: string, args: string[]): Promise<string> {
|
|
287
|
+
const { stdout } = await execFileAsync(command, args, {
|
|
288
|
+
encoding: 'utf8',
|
|
289
|
+
maxBuffer: 1024 * 1024,
|
|
290
|
+
});
|
|
291
|
+
return stdout;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function parsePsUnityProcesses(output: string): RawUnityProcess[] {
|
|
295
|
+
return output
|
|
296
|
+
.split(/\r?\n/)
|
|
297
|
+
.map((line) => line.trim())
|
|
298
|
+
.filter((line) => line.length > 0)
|
|
299
|
+
.map((line) => {
|
|
300
|
+
const match = line.match(/^(\d+)\s+(.+)$/);
|
|
301
|
+
if (match === null) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
pid: Number.parseInt(match[1], 10),
|
|
307
|
+
commandLine: match[2],
|
|
308
|
+
};
|
|
309
|
+
})
|
|
310
|
+
.filter((processInfo): processInfo is RawUnityProcess => processInfo !== null);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function parseWindowsUnityProcesses(output: string): RawUnityProcess[] {
|
|
314
|
+
const trimmed = output.trim();
|
|
315
|
+
if (trimmed.length === 0) {
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const parsed = JSON.parse(trimmed) as WindowsUnityProcessJson | WindowsUnityProcessJson[];
|
|
320
|
+
const processArray = Array.isArray(parsed) ? parsed : [parsed];
|
|
321
|
+
|
|
322
|
+
return processArray.filter(isWindowsUnityProcessWithCommandLine).map((processInfo) => ({
|
|
323
|
+
pid: processInfo.ProcessId,
|
|
324
|
+
commandLine: processInfo.CommandLine,
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
interface WindowsUnityProcessJson {
|
|
329
|
+
ProcessId: number;
|
|
330
|
+
CommandLine?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isWindowsUnityProcessWithCommandLine(
|
|
334
|
+
processInfo: WindowsUnityProcessJson,
|
|
335
|
+
): processInfo is WindowsUnityProcessJson & { CommandLine: string } {
|
|
336
|
+
return typeof processInfo.ProcessId === 'number' && typeof processInfo.CommandLine === 'string';
|
|
337
|
+
}
|
package/src/version.ts
CHANGED