uloop-cli 1.6.3 → 1.7.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 +37 -30
- package/dist/cli.bundle.cjs.map +2 -2
- package/package.json +6 -6
- package/src/__tests__/port-resolver.test.ts +45 -0
- package/src/__tests__/project-root.test.ts +35 -1
- package/src/default-tools.json +1 -1
- package/src/port-resolver.ts +39 -38
- package/src/project-root.ts +6 -1
- package/src/version.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uloop-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"//version": "x-release-please-version",
|
|
5
5
|
"description": "CLI tool for Unity Editor communication via Unity CLI Loop",
|
|
6
6
|
"main": "dist/cli.bundle.cjs",
|
|
@@ -49,17 +49,17 @@
|
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@eslint/js": "10.0.1",
|
|
51
51
|
"@types/jest": "30.0.0",
|
|
52
|
-
"@types/node": "25.5.
|
|
52
|
+
"@types/node": "25.5.2",
|
|
53
53
|
"@types/semver": "7.7.1",
|
|
54
|
-
"esbuild": "0.
|
|
55
|
-
"eslint": "10.
|
|
54
|
+
"esbuild": "0.28.0",
|
|
55
|
+
"eslint": "10.2.0",
|
|
56
56
|
"eslint-config-prettier": "10.1.8",
|
|
57
57
|
"eslint-plugin-prettier": "5.5.5",
|
|
58
58
|
"eslint-plugin-security": "4.0.0",
|
|
59
59
|
"jest": "30.3.0",
|
|
60
|
-
"knip": "6.
|
|
60
|
+
"knip": "6.3.0",
|
|
61
61
|
"prettier": "3.8.1",
|
|
62
|
-
"ts-jest": "29.4.
|
|
62
|
+
"ts-jest": "29.4.9",
|
|
63
63
|
"typescript": "5.9.3",
|
|
64
64
|
"typescript-eslint": "8.58.0"
|
|
65
65
|
},
|
|
@@ -142,4 +142,49 @@ describe('resolveUnityPort with project settings', () => {
|
|
|
142
142
|
'Could not read Unity server port from settings',
|
|
143
143
|
);
|
|
144
144
|
});
|
|
145
|
+
|
|
146
|
+
it('returns port when primary settings file is missing but backup exists', async () => {
|
|
147
|
+
writeFileSync(
|
|
148
|
+
join(tempProjectRoot, 'UserSettings/UnityMcpSettings.json.bak'),
|
|
149
|
+
JSON.stringify({ isServerRunning: true, customPort: 8722 }),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const port = await resolveUnityPort(undefined, tempProjectRoot);
|
|
153
|
+
expect(port).toBe(8722);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('returns port when primary settings file is missing but temp exists', async () => {
|
|
157
|
+
writeFileSync(
|
|
158
|
+
join(tempProjectRoot, 'UserSettings/UnityMcpSettings.json.tmp'),
|
|
159
|
+
JSON.stringify({ isServerRunning: true, customPort: 8723 }),
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const port = await resolveUnityPort(undefined, tempProjectRoot);
|
|
163
|
+
expect(port).toBe(8723);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns port from temp when both temp and backup exist', async () => {
|
|
167
|
+
writeFileSync(
|
|
168
|
+
join(tempProjectRoot, 'UserSettings/UnityMcpSettings.json.tmp'),
|
|
169
|
+
JSON.stringify({ isServerRunning: true, customPort: 8724 }),
|
|
170
|
+
);
|
|
171
|
+
writeFileSync(
|
|
172
|
+
join(tempProjectRoot, 'UserSettings/UnityMcpSettings.json.bak'),
|
|
173
|
+
JSON.stringify({ isServerRunning: true, customPort: 8725 }),
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const port = await resolveUnityPort(undefined, tempProjectRoot);
|
|
177
|
+
expect(port).toBe(8724);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('falls back to temp when primary settings file contains invalid JSON', async () => {
|
|
181
|
+
writeFileSync(join(tempProjectRoot, 'UserSettings/UnityMcpSettings.json'), 'not valid json{{{');
|
|
182
|
+
writeFileSync(
|
|
183
|
+
join(tempProjectRoot, 'UserSettings/UnityMcpSettings.json.tmp'),
|
|
184
|
+
JSON.stringify({ isServerRunning: true, customPort: 8726 }),
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const port = await resolveUnityPort(undefined, tempProjectRoot);
|
|
188
|
+
expect(port).toBe(8726);
|
|
189
|
+
});
|
|
145
190
|
});
|
|
@@ -4,7 +4,11 @@
|
|
|
4
4
|
import { mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
5
5
|
import { join } from 'path';
|
|
6
6
|
import { tmpdir } from 'os';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
findUnityProjectRoot,
|
|
9
|
+
hasUloopInstalled,
|
|
10
|
+
resetMultipleProjectsWarning,
|
|
11
|
+
} from '../project-root.js';
|
|
8
12
|
|
|
9
13
|
function createUnityProject(basePath: string, name: string): string {
|
|
10
14
|
const projectPath = join(basePath, name);
|
|
@@ -95,3 +99,33 @@ describe('findUnityProjectRoot', () => {
|
|
|
95
99
|
expect(result).toBe(join(testDir, 'Alpha'));
|
|
96
100
|
});
|
|
97
101
|
});
|
|
102
|
+
|
|
103
|
+
describe('hasUloopInstalled', () => {
|
|
104
|
+
let testDir: string;
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
testDir = join(
|
|
108
|
+
tmpdir(),
|
|
109
|
+
`uloop-installed-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
110
|
+
);
|
|
111
|
+
mkdirSync(join(testDir, 'Assets'), { recursive: true });
|
|
112
|
+
mkdirSync(join(testDir, 'ProjectSettings'), { recursive: true });
|
|
113
|
+
mkdirSync(join(testDir, 'UserSettings'), { recursive: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('returns true when only backup settings file exists', () => {
|
|
121
|
+
writeFileSync(join(testDir, 'UserSettings/UnityMcpSettings.json.bak'), '{}');
|
|
122
|
+
|
|
123
|
+
expect(hasUloopInstalled(testDir)).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns true when only temp settings file exists', () => {
|
|
127
|
+
writeFileSync(join(testDir, 'UserSettings/UnityMcpSettings.json.tmp'), '{}');
|
|
128
|
+
|
|
129
|
+
expect(hasUloopInstalled(testDir)).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
});
|
package/src/default-tools.json
CHANGED
package/src/port-resolver.ts
CHANGED
|
@@ -10,7 +10,12 @@ import { PRODUCT_DISPLAY_NAME } from './cli-constants';
|
|
|
10
10
|
import { readFile } from 'fs/promises';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { join, resolve } from 'path';
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
findUnityProjectRoot,
|
|
15
|
+
getUnitySettingsCandidatePaths,
|
|
16
|
+
isUnityProject,
|
|
17
|
+
hasUloopInstalled,
|
|
18
|
+
} from './project-root.js';
|
|
14
19
|
|
|
15
20
|
export class UnityNotRunningError extends Error {
|
|
16
21
|
constructor(public readonly projectRoot: string) {
|
|
@@ -105,41 +110,37 @@ function createSettingsReadError(projectRoot: string): Error {
|
|
|
105
110
|
|
|
106
111
|
// File I/O and JSON parsing can fail for external reasons (permissions, corruption, concurrent writes)
|
|
107
112
|
async function readPortFromSettingsOrThrow(projectRoot: string): Promise<number> {
|
|
108
|
-
const settingsPath
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
parsed
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
throw createSettingsReadError(projectRoot);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return port;
|
|
113
|
+
for (const settingsPath of getUnitySettingsCandidatePaths(projectRoot)) {
|
|
114
|
+
let content: string;
|
|
115
|
+
try {
|
|
116
|
+
content = await readFile(settingsPath, 'utf-8');
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
let parsed: unknown;
|
|
122
|
+
try {
|
|
123
|
+
parsed = JSON.parse(content);
|
|
124
|
+
} catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (typeof parsed !== 'object' || parsed === null) {
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
const settings = parsed as UnityMcpSettings;
|
|
132
|
+
|
|
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
|
+
const port = resolvePortFromUnitySettings(settings);
|
|
140
|
+
if (port !== null) {
|
|
141
|
+
return port;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
throw createSettingsReadError(projectRoot);
|
|
145
146
|
}
|
package/src/project-root.ts
CHANGED
|
@@ -28,8 +28,13 @@ export function isUnityProject(dirPath: string): boolean {
|
|
|
28
28
|
return hasAssets && hasProjectSettings;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export function getUnitySettingsCandidatePaths(dirPath: string): string[] {
|
|
32
|
+
const settingsPath = join(dirPath, 'UserSettings/UnityMcpSettings.json');
|
|
33
|
+
return [settingsPath, `${settingsPath}.tmp`, `${settingsPath}.bak`];
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
export function hasUloopInstalled(dirPath: string): boolean {
|
|
32
|
-
return
|
|
37
|
+
return getUnitySettingsCandidatePaths(dirPath).some(path => existsSync(path));
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
function isUnityProjectWithUloop(dirPath: string): boolean {
|
package/src/version.ts
CHANGED