@akiojin/unity-mcp-server 2.45.5 → 3.0.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/README.md +28 -393
- package/package.json +5 -4
- package/src/constants/offlineTools.js +19 -0
- package/src/core/codeIndex.js +63 -13
- package/src/core/config.js +41 -97
- package/src/core/projectInfo.js +45 -17
- package/src/core/server.js +255 -174
- package/src/core/stdioRpcServer.js +258 -0
- package/src/core/toolManifest.json +4436 -0
- package/src/core/workers/indexBuildWorker.js +80 -49
- package/src/handlers/script/CodeIndexBuildToolHandler.js +1 -1
- package/src/handlers/script/CodeIndexStatusToolHandler.js +1 -1
- package/src/handlers/script/CodeIndexUpdateToolHandler.js +1 -1
- package/src/handlers/script/ScriptPackagesListToolHandler.js +1 -1
- package/src/handlers/script/ScriptReadToolHandler.js +9 -2
- package/src/handlers/script/ScriptRefsFindToolHandler.js +131 -36
- package/src/handlers/script/ScriptSearchToolHandler.js +1 -1
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +32 -33
- package/src/handlers/script/ScriptSymbolsGetToolHandler.js +18 -10
- package/src/handlers/system/SystemPingToolHandler.js +30 -17
- package/src/lsp/CSharpLspUtils.js +11 -50
package/src/core/codeIndex.js
CHANGED
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { LRUCache } from 'lru-cache';
|
|
3
4
|
import { ProjectInfoProvider } from './projectInfo.js';
|
|
4
5
|
import { logger } from './config.js';
|
|
5
6
|
|
|
7
|
+
// Phase 4: Query result cache (80% reduction for repeated queries)
|
|
8
|
+
const queryCache = new LRUCache({
|
|
9
|
+
max: 500, // Max 500 cached queries
|
|
10
|
+
ttl: 1000 * 60 * 5 // 5 minute TTL
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Stats cache with shorter TTL
|
|
14
|
+
const statsCache = new LRUCache({
|
|
15
|
+
max: 1,
|
|
16
|
+
ttl: 1000 * 60 // 1 minute TTL for stats
|
|
17
|
+
});
|
|
18
|
+
|
|
6
19
|
// fast-sql helper: execute query and return results
|
|
7
20
|
function querySQL(db, sql) {
|
|
8
21
|
return db.execSql(sql);
|
|
@@ -138,7 +151,10 @@ export class CodeIndex {
|
|
|
138
151
|
|
|
139
152
|
_initSchema() {
|
|
140
153
|
if (!this.db) return;
|
|
141
|
-
//
|
|
154
|
+
// Phase 5: Explicit PRAGMA optimization
|
|
155
|
+
this.db.run('PRAGMA cache_size = 16000'); // 64MB cache (16000 * 4KB pages)
|
|
156
|
+
this.db.run('PRAGMA temp_store = MEMORY'); // Faster temp operations
|
|
157
|
+
this.db.run('PRAGMA synchronous = NORMAL'); // Balanced safety/speed
|
|
142
158
|
this.db.run(`
|
|
143
159
|
CREATE TABLE IF NOT EXISTS meta (
|
|
144
160
|
key TEXT PRIMARY KEY,
|
|
@@ -166,6 +182,9 @@ export class CodeIndex {
|
|
|
166
182
|
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)');
|
|
167
183
|
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)');
|
|
168
184
|
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path)');
|
|
185
|
+
// Composite indexes for faster multi-condition queries
|
|
186
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_name_kind ON symbols(name, kind)');
|
|
187
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_path_name ON symbols(path, name)');
|
|
169
188
|
this._saveToFile();
|
|
170
189
|
}
|
|
171
190
|
|
|
@@ -181,6 +200,10 @@ export class CodeIndex {
|
|
|
181
200
|
const db = await this.open();
|
|
182
201
|
if (!db) throw new Error('CodeIndex is unavailable (fast-sql not loaded)');
|
|
183
202
|
|
|
203
|
+
// Phase 4: Invalidate caches on write
|
|
204
|
+
queryCache.clear();
|
|
205
|
+
statsCache.clear();
|
|
206
|
+
|
|
184
207
|
db.run('BEGIN TRANSACTION');
|
|
185
208
|
try {
|
|
186
209
|
db.run('DELETE FROM symbols');
|
|
@@ -232,6 +255,8 @@ export class CodeIndex {
|
|
|
232
255
|
async upsertFile(pathStr, sig) {
|
|
233
256
|
const db = await this.open();
|
|
234
257
|
if (!db) return;
|
|
258
|
+
// Phase 4: Invalidate caches on write
|
|
259
|
+
statsCache.clear();
|
|
235
260
|
const stmt = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
236
261
|
stmt.run([pathStr, sig || '', new Date().toISOString()]);
|
|
237
262
|
stmt.free();
|
|
@@ -241,6 +266,9 @@ export class CodeIndex {
|
|
|
241
266
|
async removeFile(pathStr) {
|
|
242
267
|
const db = await this.open();
|
|
243
268
|
if (!db) return;
|
|
269
|
+
// Phase 4: Invalidate caches on write
|
|
270
|
+
queryCache.clear();
|
|
271
|
+
statsCache.clear();
|
|
244
272
|
db.run('BEGIN TRANSACTION');
|
|
245
273
|
try {
|
|
246
274
|
const stmt1 = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
@@ -263,6 +291,10 @@ export class CodeIndex {
|
|
|
263
291
|
const db = await this.open();
|
|
264
292
|
if (!db) return;
|
|
265
293
|
|
|
294
|
+
// Phase 4: Invalidate caches on write
|
|
295
|
+
queryCache.clear();
|
|
296
|
+
statsCache.clear();
|
|
297
|
+
|
|
266
298
|
db.run('BEGIN TRANSACTION');
|
|
267
299
|
try {
|
|
268
300
|
const delStmt = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
@@ -298,6 +330,11 @@ export class CodeIndex {
|
|
|
298
330
|
}
|
|
299
331
|
|
|
300
332
|
async querySymbols({ name, kind, scope = 'all', exact = false }) {
|
|
333
|
+
// Phase 4: Check cache first
|
|
334
|
+
const cacheKey = JSON.stringify({ name, kind, scope, exact });
|
|
335
|
+
const cached = queryCache.get(cacheKey);
|
|
336
|
+
if (cached) return cached;
|
|
337
|
+
|
|
301
338
|
const db = await this.open();
|
|
302
339
|
if (!db) return [];
|
|
303
340
|
|
|
@@ -318,6 +355,16 @@ export class CodeIndex {
|
|
|
318
355
|
params.push(kind);
|
|
319
356
|
}
|
|
320
357
|
|
|
358
|
+
// Apply scope filter directly in SQL for better performance
|
|
359
|
+
if (scope === 'assets') {
|
|
360
|
+
sql += " AND path LIKE 'Assets/%'";
|
|
361
|
+
} else if (scope === 'packages') {
|
|
362
|
+
sql += " AND (path LIKE 'Packages/%' OR path LIKE '%Library/PackageCache/%')";
|
|
363
|
+
} else if (scope === 'embedded') {
|
|
364
|
+
sql += " AND path LIKE 'Packages/%'";
|
|
365
|
+
}
|
|
366
|
+
// scope === 'all' requires no additional filter
|
|
367
|
+
|
|
321
368
|
const stmt = db.prepare(sql);
|
|
322
369
|
if (params.length > 0) {
|
|
323
370
|
stmt.bind(params);
|
|
@@ -330,17 +377,7 @@ export class CodeIndex {
|
|
|
330
377
|
}
|
|
331
378
|
stmt.free();
|
|
332
379
|
|
|
333
|
-
|
|
334
|
-
const filtered = rows.filter(r => {
|
|
335
|
-
const p = String(r.path || '').replace(/\\/g, '/');
|
|
336
|
-
if (scope === 'assets') return p.startsWith('Assets/');
|
|
337
|
-
if (scope === 'packages')
|
|
338
|
-
return p.startsWith('Packages/') || p.includes('Library/PackageCache/');
|
|
339
|
-
if (scope === 'embedded') return p.startsWith('Packages/');
|
|
340
|
-
return true;
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
return filtered.map(r => ({
|
|
380
|
+
const result = rows.map(r => ({
|
|
344
381
|
path: r.path,
|
|
345
382
|
name: r.name,
|
|
346
383
|
kind: r.kind,
|
|
@@ -349,9 +386,17 @@ export class CodeIndex {
|
|
|
349
386
|
line: r.line,
|
|
350
387
|
column: r.column
|
|
351
388
|
}));
|
|
389
|
+
|
|
390
|
+
// Cache the result
|
|
391
|
+
queryCache.set(cacheKey, result);
|
|
392
|
+
return result;
|
|
352
393
|
}
|
|
353
394
|
|
|
354
395
|
async getStats() {
|
|
396
|
+
// Phase 4: Check stats cache first
|
|
397
|
+
const cached = statsCache.get('stats');
|
|
398
|
+
if (cached) return cached;
|
|
399
|
+
|
|
355
400
|
const db = await this.open();
|
|
356
401
|
if (!db) return { total: 0, lastIndexedAt: null };
|
|
357
402
|
|
|
@@ -363,7 +408,9 @@ export class CodeIndex {
|
|
|
363
408
|
const last =
|
|
364
409
|
metaResult.length > 0 && metaResult[0].values.length > 0 ? metaResult[0].values[0][0] : null;
|
|
365
410
|
|
|
366
|
-
|
|
411
|
+
const result = { total, lastIndexedAt: last };
|
|
412
|
+
statsCache.set('stats', result);
|
|
413
|
+
return result;
|
|
367
414
|
}
|
|
368
415
|
|
|
369
416
|
/**
|
|
@@ -387,6 +434,9 @@ export function __resetCodeIndexDriverStatusForTest() {
|
|
|
387
434
|
driverStatus.available = null;
|
|
388
435
|
driverStatus.error = null;
|
|
389
436
|
driverStatus.logged = false;
|
|
437
|
+
// Phase 4: Clear query caches
|
|
438
|
+
queryCache.clear();
|
|
439
|
+
statsCache.clear();
|
|
390
440
|
// Also reset shared connections
|
|
391
441
|
if (sharedConnections.db) {
|
|
392
442
|
try {
|
package/src/core/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import * as findUpPkg from 'find-up';
|
|
4
5
|
import { MCPLogger } from './mcpLogger.js';
|
|
@@ -67,20 +68,13 @@ function resolvePackageVersion() {
|
|
|
67
68
|
/**
|
|
68
69
|
* Base configuration for Unity MCP Server Server
|
|
69
70
|
*/
|
|
70
|
-
const envUnityHost = process.env.UNITY_BIND_HOST || process.env.UNITY_HOST || null;
|
|
71
|
-
|
|
72
|
-
const envMcpHost =
|
|
73
|
-
process.env.UNITY_MCP_HOST || process.env.UNITY_CLIENT_HOST || process.env.UNITY_HOST || null;
|
|
74
|
-
|
|
75
|
-
const envBindHost = process.env.UNITY_BIND_HOST || null;
|
|
76
|
-
|
|
77
71
|
const baseConfig = {
|
|
78
72
|
// Unity connection settings
|
|
79
73
|
unity: {
|
|
80
|
-
unityHost:
|
|
81
|
-
mcpHost:
|
|
82
|
-
bindHost:
|
|
83
|
-
port:
|
|
74
|
+
unityHost: null,
|
|
75
|
+
mcpHost: null,
|
|
76
|
+
bindHost: null,
|
|
77
|
+
port: 6400,
|
|
84
78
|
reconnectDelay: 1000,
|
|
85
79
|
maxReconnectDelay: 30000,
|
|
86
80
|
reconnectBackoffMultiplier: 2,
|
|
@@ -96,24 +90,21 @@ const baseConfig = {
|
|
|
96
90
|
|
|
97
91
|
// Logging settings
|
|
98
92
|
logging: {
|
|
99
|
-
level:
|
|
93
|
+
level: 'info',
|
|
100
94
|
prefix: '[unity-mcp-server]'
|
|
101
95
|
},
|
|
102
96
|
|
|
103
97
|
// HTTP transport (off by default)
|
|
104
98
|
http: {
|
|
105
|
-
enabled:
|
|
106
|
-
host:
|
|
107
|
-
port:
|
|
99
|
+
enabled: false,
|
|
100
|
+
host: '0.0.0.0',
|
|
101
|
+
port: 6401,
|
|
108
102
|
healthPath: '/healthz',
|
|
109
|
-
allowedHosts:
|
|
110
|
-
.split(',')
|
|
111
|
-
.map(h => h.trim())
|
|
112
|
-
.filter(h => h.length > 0)
|
|
103
|
+
allowedHosts: ['localhost', '127.0.0.1']
|
|
113
104
|
},
|
|
114
105
|
|
|
115
106
|
telemetry: {
|
|
116
|
-
enabled:
|
|
107
|
+
enabled: false,
|
|
117
108
|
destinations: [],
|
|
118
109
|
fields: []
|
|
119
110
|
},
|
|
@@ -123,13 +114,13 @@ const baseConfig = {
|
|
|
123
114
|
// Search-related defaults and engine selection
|
|
124
115
|
search: {
|
|
125
116
|
// detail alias: 'compact' maps to returnMode 'snippets'
|
|
126
|
-
defaultDetail:
|
|
127
|
-
engine:
|
|
117
|
+
defaultDetail: 'compact', // compact|metadata|snippets|full
|
|
118
|
+
engine: 'naive' // naive|treesitter (future)
|
|
128
119
|
},
|
|
129
120
|
|
|
130
121
|
// LSP client defaults
|
|
131
122
|
lsp: {
|
|
132
|
-
requestTimeoutMs:
|
|
123
|
+
requestTimeoutMs: 60000
|
|
133
124
|
},
|
|
134
125
|
|
|
135
126
|
// Indexing (code index) settings
|
|
@@ -137,95 +128,48 @@ const baseConfig = {
|
|
|
137
128
|
// Enable periodic incremental index updates (polling watcher)
|
|
138
129
|
watch: true,
|
|
139
130
|
// Polling interval (ms)
|
|
140
|
-
intervalMs:
|
|
131
|
+
intervalMs: 15000,
|
|
141
132
|
// Build options
|
|
142
|
-
concurrency:
|
|
143
|
-
retry:
|
|
144
|
-
reportEvery:
|
|
133
|
+
concurrency: 8,
|
|
134
|
+
retry: 2,
|
|
135
|
+
reportEvery: 500
|
|
145
136
|
}
|
|
146
137
|
};
|
|
147
138
|
|
|
148
139
|
/**
|
|
149
|
-
* External config resolution
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
* 2) ./.unity/config.json (project-local)
|
|
153
|
-
* 3) ~/.unity/config.json (user-global)
|
|
154
|
-
* If none found, create ./.unity/config.json with defaults.
|
|
140
|
+
* External config resolution:
|
|
141
|
+
* - Uses the nearest `.unity/config.json` found by walking up from the current working directory.
|
|
142
|
+
* - Intentionally ignores `~/.unity/config.json` (user-global).
|
|
155
143
|
*/
|
|
156
|
-
function ensureDefaultProjectConfig(baseDir) {
|
|
157
|
-
const dir = path.resolve(baseDir, '.unity');
|
|
158
|
-
const file = path.join(dir, 'config.json');
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
if (!fs.existsSync(dir)) {
|
|
162
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!fs.existsSync(file)) {
|
|
166
|
-
const inferredRoot = fs.existsSync(path.join(baseDir, 'Assets')) ? baseDir : '';
|
|
167
|
-
const defaultConfig = {
|
|
168
|
-
unity: {
|
|
169
|
-
unityHost: 'localhost',
|
|
170
|
-
mcpHost: 'localhost',
|
|
171
|
-
port: 6400
|
|
172
|
-
},
|
|
173
|
-
project: {
|
|
174
|
-
root: inferredRoot ? inferredRoot.replace(/\\/g, '/') : ''
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
fs.writeFileSync(file, `${JSON.stringify(defaultConfig, null, 2)}\n`, 'utf8');
|
|
178
|
-
}
|
|
179
|
-
return file;
|
|
180
|
-
} catch (error) {
|
|
181
|
-
return null;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
144
|
function loadExternalConfig() {
|
|
186
145
|
if (typeof findUpSyncCompat !== 'function') {
|
|
187
146
|
return {};
|
|
188
147
|
}
|
|
189
|
-
|
|
148
|
+
let userGlobalPath = null;
|
|
149
|
+
try {
|
|
150
|
+
const homeDir = os.homedir();
|
|
151
|
+
userGlobalPath = homeDir ? path.resolve(homeDir, '.unity', 'config.json') : null;
|
|
152
|
+
} catch {}
|
|
190
153
|
|
|
191
154
|
const projectPath = findUpSyncCompat(
|
|
192
155
|
directory => {
|
|
193
156
|
const candidate = path.resolve(directory, '.unity', 'config.json');
|
|
157
|
+
if (userGlobalPath && path.resolve(candidate) === userGlobalPath) return undefined;
|
|
194
158
|
return fs.existsSync(candidate) ? candidate : undefined;
|
|
195
159
|
},
|
|
196
160
|
{ cwd: process.cwd() }
|
|
197
161
|
);
|
|
198
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE || '';
|
|
199
|
-
const userPath = homeDir ? path.resolve(homeDir, '.unity', 'config.json') : null;
|
|
200
162
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
} catch (e) {
|
|
212
|
-
return { __configLoadError: `${p}: ${e.message}` };
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
const fallbackPath = ensureDefaultProjectConfig(process.cwd());
|
|
216
|
-
if (fallbackPath && fs.existsSync(fallbackPath)) {
|
|
217
|
-
try {
|
|
218
|
-
const raw = fs.readFileSync(fallbackPath, 'utf8');
|
|
219
|
-
const json = JSON.parse(raw);
|
|
220
|
-
const out = json && typeof json === 'object' ? json : {};
|
|
221
|
-
out.__configPath = fallbackPath;
|
|
222
|
-
out.__configGenerated = true;
|
|
223
|
-
return out;
|
|
224
|
-
} catch (e) {
|
|
225
|
-
return { __configLoadError: `${fallbackPath}: ${e.message}` };
|
|
226
|
-
}
|
|
163
|
+
if (!projectPath) return {};
|
|
164
|
+
try {
|
|
165
|
+
const raw = fs.readFileSync(projectPath, 'utf8');
|
|
166
|
+
const json = JSON.parse(raw);
|
|
167
|
+
const out = json && typeof json === 'object' ? json : {};
|
|
168
|
+
out.__configPath = projectPath;
|
|
169
|
+
return out;
|
|
170
|
+
} catch (e) {
|
|
171
|
+
return { __configLoadError: `${projectPath}: ${e.message}` };
|
|
227
172
|
}
|
|
228
|
-
return {};
|
|
229
173
|
}
|
|
230
174
|
|
|
231
175
|
const external = loadExternalConfig();
|
|
@@ -234,22 +178,22 @@ export const config = merge(baseConfig, external);
|
|
|
234
178
|
const normalizeUnityConfig = () => {
|
|
235
179
|
const unityConfig = config.unity || (config.unity = {});
|
|
236
180
|
|
|
237
|
-
// Legacy aliases coming from config files
|
|
181
|
+
// Legacy aliases coming from config files
|
|
238
182
|
const legacyHost = unityConfig.host;
|
|
239
183
|
const legacyClientHost = unityConfig.clientHost;
|
|
240
184
|
const legacyBindHost = unityConfig.bindHost;
|
|
241
185
|
|
|
242
186
|
if (!unityConfig.unityHost) {
|
|
243
|
-
unityConfig.unityHost = legacyBindHost || legacyHost ||
|
|
187
|
+
unityConfig.unityHost = legacyBindHost || legacyHost || 'localhost';
|
|
244
188
|
}
|
|
245
189
|
|
|
246
190
|
if (!unityConfig.mcpHost) {
|
|
247
|
-
unityConfig.mcpHost = legacyClientHost ||
|
|
191
|
+
unityConfig.mcpHost = legacyClientHost || legacyHost || unityConfig.unityHost;
|
|
248
192
|
}
|
|
249
193
|
|
|
250
194
|
// Keep bindHost for backwards compatibility with legacy code paths
|
|
251
195
|
if (!unityConfig.bindHost) {
|
|
252
|
-
unityConfig.bindHost = legacyBindHost ||
|
|
196
|
+
unityConfig.bindHost = legacyBindHost || unityConfig.unityHost;
|
|
253
197
|
}
|
|
254
198
|
|
|
255
199
|
// Maintain legacy properties so older handlers keep working
|
|
@@ -291,7 +235,7 @@ if (config.__configLoadError) {
|
|
|
291
235
|
// Startup debug log: output config info to stderr for troubleshooting
|
|
292
236
|
// This helps diagnose connection issues (especially in WSL2/Docker environments)
|
|
293
237
|
console.error(`[unity-mcp-server] Startup config:`);
|
|
294
|
-
console.error(`[unity-mcp-server] Config file: ${config.__configPath || '(
|
|
238
|
+
console.error(`[unity-mcp-server] Config file: ${config.__configPath || '(not found)'}`);
|
|
295
239
|
console.error(
|
|
296
240
|
`[unity-mcp-server] Unity host: ${config.unity.mcpHost || config.unity.unityHost || 'localhost'}`
|
|
297
241
|
);
|
package/src/core/projectInfo.js
CHANGED
|
@@ -1,8 +1,36 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
1
2
|
import path from 'path';
|
|
2
3
|
import { logger, config, WORKSPACE_ROOT } from './config.js';
|
|
3
4
|
|
|
4
5
|
const normalize = p => p.replace(/\\/g, '/');
|
|
5
6
|
|
|
7
|
+
const looksLikeUnityProjectRoot = dir => {
|
|
8
|
+
try {
|
|
9
|
+
return (
|
|
10
|
+
fs.existsSync(path.join(dir, 'Assets')) &&
|
|
11
|
+
fs.existsSync(path.join(dir, 'Packages')) &&
|
|
12
|
+
fs.statSync(path.join(dir, 'Assets')).isDirectory() &&
|
|
13
|
+
fs.statSync(path.join(dir, 'Packages')).isDirectory()
|
|
14
|
+
);
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const inferUnityProjectRootFromDir = startDir => {
|
|
21
|
+
try {
|
|
22
|
+
let dir = startDir;
|
|
23
|
+
const { root } = path.parse(dir);
|
|
24
|
+
while (true) {
|
|
25
|
+
if (looksLikeUnityProjectRoot(dir)) return dir;
|
|
26
|
+
if (dir === root) return null;
|
|
27
|
+
dir = path.dirname(dir);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
6
34
|
const resolveDefaultCodeIndexRoot = projectRoot => {
|
|
7
35
|
const base = WORKSPACE_ROOT || projectRoot || process.cwd();
|
|
8
36
|
return normalize(path.join(base, '.unity', 'cache', 'code-index'));
|
|
@@ -18,21 +46,6 @@ export class ProjectInfoProvider {
|
|
|
18
46
|
|
|
19
47
|
async get() {
|
|
20
48
|
if (this.cached) return this.cached;
|
|
21
|
-
// Env-driven project root override (primarily for tests)
|
|
22
|
-
const envRootRaw = process.env.UNITY_PROJECT_ROOT;
|
|
23
|
-
if (typeof envRootRaw === 'string' && envRootRaw.trim().length > 0) {
|
|
24
|
-
const envRoot = envRootRaw.trim();
|
|
25
|
-
const projectRoot = normalize(path.resolve(envRoot));
|
|
26
|
-
const codeIndexRoot = normalize(resolveDefaultCodeIndexRoot(projectRoot));
|
|
27
|
-
this.cached = {
|
|
28
|
-
projectRoot,
|
|
29
|
-
assetsPath: normalize(path.join(projectRoot, 'Assets')),
|
|
30
|
-
packagesPath: normalize(path.join(projectRoot, 'Packages')),
|
|
31
|
-
codeIndexRoot
|
|
32
|
-
};
|
|
33
|
-
return this.cached;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
49
|
// Config-driven project root (no env fallback)
|
|
37
50
|
const cfgRootRaw = config?.project?.root;
|
|
38
51
|
if (typeof cfgRootRaw === 'string' && cfgRootRaw.trim().length > 0) {
|
|
@@ -73,13 +86,28 @@ export class ProjectInfoProvider {
|
|
|
73
86
|
logger.warning(`get_editor_info failed: ${e.message}`);
|
|
74
87
|
}
|
|
75
88
|
}
|
|
89
|
+
|
|
90
|
+
// Best-effort local inference (when Unity isn't connected)
|
|
91
|
+
const inferredRoot = inferUnityProjectRootFromDir(process.cwd());
|
|
92
|
+
if (inferredRoot) {
|
|
93
|
+
const projectRoot = normalize(inferredRoot);
|
|
94
|
+
const codeIndexRoot = normalize(resolveDefaultCodeIndexRoot(projectRoot));
|
|
95
|
+
this.cached = {
|
|
96
|
+
projectRoot,
|
|
97
|
+
assetsPath: normalize(path.join(projectRoot, 'Assets')),
|
|
98
|
+
packagesPath: normalize(path.join(projectRoot, 'Packages')),
|
|
99
|
+
codeIndexRoot
|
|
100
|
+
};
|
|
101
|
+
return this.cached;
|
|
102
|
+
}
|
|
103
|
+
|
|
76
104
|
if (typeof cfgRootRaw === 'string') {
|
|
77
105
|
throw new Error(
|
|
78
|
-
'project.root is configured but empty. Set a valid path in .unity/config.json
|
|
106
|
+
'project.root is configured but empty. Set a valid path in .unity/config.json.'
|
|
79
107
|
);
|
|
80
108
|
}
|
|
81
109
|
throw new Error(
|
|
82
|
-
'Unable to resolve Unity project root.
|
|
110
|
+
'Unable to resolve Unity project root. Start the server inside a Unity project (directory containing Assets/ and Packages/) or configure project.root in .unity/config.json.'
|
|
83
111
|
);
|
|
84
112
|
}
|
|
85
113
|
|