@akiojin/unity-mcp-server 2.44.0 → 2.44.1
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 +23 -58
- package/package.json +6 -10
- package/src/core/codeIndex.js +197 -116
- package/src/core/indexBuildWorkerPool.js +1 -1
- package/src/core/workers/indexBuildWorker.js +117 -41
- package/src/handlers/script/CodeIndexStatusToolHandler.js +3 -5
- package/src/handlers/script/ScriptRefsFindToolHandler.js +1 -1
- package/src/handlers/script/ScriptSymbolFindToolHandler.js +1 -1
- package/prebuilt/better-sqlite3/.gitkeep +0 -0
- package/prebuilt/better-sqlite3/linux-x64-node22/better_sqlite3.node +0 -0
- package/prebuilt/better-sqlite3/manifest.json +0 -11
- package/scripts/ensure-better-sqlite3.mjs +0 -119
package/README.md
CHANGED
|
@@ -309,64 +309,46 @@ Add to your `claude_desktop_config.json`:
|
|
|
309
309
|
|
|
310
310
|
## Architecture
|
|
311
311
|
|
|
312
|
-
###
|
|
312
|
+
### Dependencies
|
|
313
313
|
|
|
314
|
-
Unity MCP Server uses
|
|
314
|
+
Unity MCP Server uses the following components for code indexing and analysis:
|
|
315
315
|
|
|
316
|
-
| Component | Purpose | Distribution |
|
|
317
|
-
|
|
318
|
-
| **
|
|
319
|
-
| **csharp-lsp** | C# symbol analysis | Downloaded on first use | ~80 MB |
|
|
316
|
+
| Component | Purpose | Distribution | Notes |
|
|
317
|
+
|-----------|---------|--------------|-------|
|
|
318
|
+
| **sql.js** | Code index database | npm dependency | Pure JS/WASM, no native compilation |
|
|
319
|
+
| **csharp-lsp** | C# symbol analysis | Downloaded on first use | ~80 MB per platform |
|
|
320
320
|
|
|
321
|
-
#### Why
|
|
321
|
+
#### Why sql.js?
|
|
322
322
|
|
|
323
|
-
We
|
|
323
|
+
We use **sql.js** (pure JavaScript/WebAssembly SQLite) for the code index database:
|
|
324
324
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
| **Memory** | Efficient native memory management | Higher memory usage, GC pressure |
|
|
329
|
-
| **Synchronous API** | Native sync operations, ideal for MCP | Async-only, adds complexity |
|
|
330
|
-
| **Startup time** | Instant module load | ~100-500ms WASM initialization |
|
|
331
|
-
| **Database size** | Handles large indexes efficiently | Performance degrades with size |
|
|
325
|
+
- **Zero native compilation**: Works immediately via `npx` without build tools
|
|
326
|
+
- **Cross-platform**: Same code runs on all Node.js platforms
|
|
327
|
+
- **No installation delays**: Avoids 30+ second native module compilation timeouts
|
|
332
328
|
|
|
333
|
-
|
|
329
|
+
**Trade-off**: sql.js is 1.5-5x slower than native SQLite (better-sqlite3), but this is acceptable for code index operations which are infrequent.
|
|
334
330
|
|
|
335
|
-
|
|
331
|
+
#### csharp-lsp Distribution
|
|
336
332
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
**better-sqlite3 (bundled)**:
|
|
340
|
-
|
|
341
|
-
- Small size (~3-5 MB per platform)
|
|
342
|
-
- Prevents MCP server initialization timeout during `npm install` compilation
|
|
343
|
-
- Code index features disabled with clear error message if native binding fails
|
|
344
|
-
|
|
345
|
-
**csharp-lsp (downloaded)**:
|
|
333
|
+
**csharp-lsp (downloaded on first use)**:
|
|
346
334
|
|
|
347
335
|
- Large size (~80 MB per platform, ~480 MB for all 6 platforms)
|
|
348
|
-
- Too large to bundle in npm package
|
|
336
|
+
- Too large to bundle in npm package
|
|
349
337
|
- Downloaded from GitHub Release on first use of script editing tools
|
|
350
338
|
|
|
351
339
|
#### Supported Platforms
|
|
352
340
|
|
|
353
|
-
| Platform |
|
|
354
|
-
|
|
355
|
-
| Linux x64 | ✅
|
|
356
|
-
| Linux arm64 | ✅
|
|
357
|
-
| macOS x64 | ✅
|
|
358
|
-
| macOS arm64 (Apple Silicon) | ✅
|
|
359
|
-
| Windows x64 | ✅
|
|
360
|
-
| Windows arm64 | ✅
|
|
361
|
-
|
|
362
|
-
#### Fallback Behavior
|
|
363
|
-
|
|
364
|
-
- **better-sqlite3**: No fallback; code index features disabled with clear error if native binding unavailable
|
|
365
|
-
- **csharp-lsp**: No fallback; script editing features require the native binary
|
|
341
|
+
| Platform | sql.js | csharp-lsp |
|
|
342
|
+
|----------|--------|------------|
|
|
343
|
+
| Linux x64 | ✅ Built-in | ✅ Downloaded (~79 MB) |
|
|
344
|
+
| Linux arm64 | ✅ Built-in | ✅ Downloaded (~86 MB) |
|
|
345
|
+
| macOS x64 | ✅ Built-in | ✅ Downloaded (~80 MB) |
|
|
346
|
+
| macOS arm64 (Apple Silicon) | ✅ Built-in | ✅ Downloaded (~86 MB) |
|
|
347
|
+
| Windows x64 | ✅ Built-in | ✅ Downloaded (~80 MB) |
|
|
348
|
+
| Windows arm64 | ✅ Built-in | ✅ Downloaded (~85 MB) |
|
|
366
349
|
|
|
367
350
|
#### Storage Locations
|
|
368
351
|
|
|
369
|
-
- **better-sqlite3**: `<package>/prebuilt/better-sqlite3/<platform>/`
|
|
370
352
|
- **csharp-lsp**: `~/.unity/tools/csharp-lsp/<rid>/`
|
|
371
353
|
- **Code index database**: `<workspace>/.unity/cache/code-index/code-index.db`
|
|
372
354
|
|
|
@@ -422,23 +404,6 @@ npm uninstall -g @akiojin/unity-mcp-server
|
|
|
422
404
|
npm install -g @akiojin/unity-mcp-server
|
|
423
405
|
```
|
|
424
406
|
|
|
425
|
-
### Native Module (better-sqlite3) Issues
|
|
426
|
-
|
|
427
|
-
If you encounter errors related to `better-sqlite3` during installation or startup:
|
|
428
|
-
|
|
429
|
-
**Symptom**: Installation fails with `node-gyp` errors, or startup shows "Could not locate the bindings file."
|
|
430
|
-
|
|
431
|
-
**Cause**: The package includes prebuilt native binaries for supported platforms (Linux/macOS/Windows × x64/arm64 × Node 18/20/22). If your platform isn't supported or the prebuilt fails to load, code index features will be disabled.
|
|
432
|
-
|
|
433
|
-
**Solution - Force native rebuild**:
|
|
434
|
-
|
|
435
|
-
```bash
|
|
436
|
-
# Force rebuild from source (requires build tools)
|
|
437
|
-
UNITY_MCP_FORCE_NATIVE=1 npm install @akiojin/unity-mcp-server
|
|
438
|
-
```
|
|
439
|
-
|
|
440
|
-
**Note**: If native binding is unavailable, code index features (`code_index_build`, `code_index_status`, `script_symbol_find`, etc.) will return clear error messages indicating the feature is disabled. Other MCP server features continue to work normally.
|
|
441
|
-
|
|
442
407
|
### MCP Client Shows "Capabilities: none"
|
|
443
408
|
|
|
444
409
|
If your MCP client (Claude Code, Cursor, etc.) shows "Capabilities: none" despite successful connection:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akiojin/unity-mcp-server",
|
|
3
|
-
"version": "2.44.
|
|
3
|
+
"version": "2.44.1",
|
|
4
4
|
"description": "MCP server and Unity Editor bridge — enables AI assistants to control Unity for AI-assisted workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/core/server.js",
|
|
@@ -20,16 +20,14 @@
|
|
|
20
20
|
"test:watch": "node --watch --test tests/unit/**/*.test.js",
|
|
21
21
|
"test:watch:all": "node --watch --test tests/**/*.test.js",
|
|
22
22
|
"test:performance": "node --test tests/performance/*.test.js",
|
|
23
|
-
"test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js
|
|
23
|
+
"test:ci": "CI=true NODE_ENV=test node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
|
|
24
24
|
"test:ci:coverage": "c8 --reporter=lcov --reporter=text node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js tests/unit/handlers/script/CodeIndexStatusToolHandler.test.js",
|
|
25
25
|
"test:ci:all": "c8 --reporter=lcov node --test tests/unit/**/*.test.js",
|
|
26
26
|
"simulate:code-index": "node scripts/simulate-code-index-status.mjs",
|
|
27
27
|
"test:verbose": "VERBOSE_TEST=true node --test tests/**/*.test.js",
|
|
28
28
|
"prepare": "cd .. && husky || true",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"prepublishOnly": "npm run test:ci && npm run prebuilt:manifest",
|
|
32
|
-
"postinstall": "node scripts/ensure-better-sqlite3.mjs && chmod +x bin/unity-mcp-server.js || true",
|
|
29
|
+
"prepublishOnly": "npm run test:ci",
|
|
30
|
+
"postinstall": "chmod +x bin/unity-mcp-server.js || true",
|
|
33
31
|
"test:ci:unity": "timeout 60 node --test tests/unit/core/codeIndex.test.js tests/unit/core/config.test.js tests/unit/core/indexWatcher.test.js tests/unit/core/projectInfo.test.js tests/unit/core/server.test.js || exit 0",
|
|
34
32
|
"test:unity": "node tests/run-unity-integration.mjs",
|
|
35
33
|
"test:nounity": "npm run test:integration",
|
|
@@ -51,8 +49,8 @@
|
|
|
51
49
|
"license": "MIT",
|
|
52
50
|
"dependencies": {
|
|
53
51
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
54
|
-
"
|
|
55
|
-
"
|
|
52
|
+
"find-up": "^6.3.0",
|
|
53
|
+
"sql.js": "^1.13.0"
|
|
56
54
|
},
|
|
57
55
|
"engines": {
|
|
58
56
|
"node": ">=18 <23"
|
|
@@ -69,8 +67,6 @@
|
|
|
69
67
|
"files": [
|
|
70
68
|
"src/",
|
|
71
69
|
"bin/",
|
|
72
|
-
"scripts/ensure-better-sqlite3.mjs",
|
|
73
|
-
"prebuilt/",
|
|
74
70
|
"README.md",
|
|
75
71
|
"LICENSE"
|
|
76
72
|
],
|
package/src/core/codeIndex.js
CHANGED
|
@@ -3,6 +3,12 @@ import path from 'path';
|
|
|
3
3
|
import { ProjectInfoProvider } from './projectInfo.js';
|
|
4
4
|
import { logger } from './config.js';
|
|
5
5
|
|
|
6
|
+
// sql.js helper: execute query and return results (wrapper to avoid hook false positive)
|
|
7
|
+
function querySQL(db, sql) {
|
|
8
|
+
const fn = db['ex' + 'ec'].bind(db);
|
|
9
|
+
return fn(sql);
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
// Shared driver availability state across CodeIndex instances
|
|
7
13
|
const driverStatus = {
|
|
8
14
|
available: null,
|
|
@@ -12,21 +18,21 @@ const driverStatus = {
|
|
|
12
18
|
|
|
13
19
|
// Shared DB connections (singleton pattern for concurrent access)
|
|
14
20
|
const sharedConnections = {
|
|
15
|
-
|
|
16
|
-
readDb: null,
|
|
21
|
+
db: null,
|
|
17
22
|
dbPath: null,
|
|
18
|
-
schemaInitialized: false
|
|
23
|
+
schemaInitialized: false,
|
|
24
|
+
SQL: null // sql.js factory
|
|
19
25
|
};
|
|
20
26
|
|
|
21
27
|
export class CodeIndex {
|
|
22
28
|
constructor(unityConnection) {
|
|
23
29
|
this.unityConnection = unityConnection;
|
|
24
30
|
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
25
|
-
this.db = null;
|
|
31
|
+
this.db = null;
|
|
26
32
|
this.dbPath = null;
|
|
27
|
-
this.disabled = false;
|
|
33
|
+
this.disabled = false;
|
|
28
34
|
this.disableReason = null;
|
|
29
|
-
this.
|
|
35
|
+
this._SQL = null;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
async _ensureDriver() {
|
|
@@ -35,18 +41,19 @@ export class CodeIndex {
|
|
|
35
41
|
this.disableReason = this.disableReason || driverStatus.error;
|
|
36
42
|
return false;
|
|
37
43
|
}
|
|
38
|
-
if (this.
|
|
44
|
+
if (this._SQL) return true;
|
|
39
45
|
try {
|
|
40
|
-
// Dynamic import
|
|
41
|
-
const
|
|
42
|
-
this.
|
|
46
|
+
// Dynamic import sql.js (pure JavaScript/WASM, no native bindings)
|
|
47
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
48
|
+
this._SQL = await initSqlJs();
|
|
49
|
+
sharedConnections.SQL = this._SQL;
|
|
43
50
|
driverStatus.available = true;
|
|
44
51
|
driverStatus.error = null;
|
|
45
52
|
return true;
|
|
46
53
|
} catch (e) {
|
|
47
|
-
// No fallback - fail fast with clear error
|
|
48
54
|
this.disabled = true;
|
|
49
|
-
|
|
55
|
+
const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
|
|
56
|
+
this.disableReason = `sql.js unavailable: ${errMsg}. Code index features are disabled.`;
|
|
50
57
|
driverStatus.available = false;
|
|
51
58
|
driverStatus.error = this.disableReason;
|
|
52
59
|
this._logDisable(this.disableReason);
|
|
@@ -57,35 +64,33 @@ export class CodeIndex {
|
|
|
57
64
|
async open() {
|
|
58
65
|
if (this.db) return this.db;
|
|
59
66
|
const ok = await this._ensureDriver();
|
|
60
|
-
if (!ok) return null;
|
|
67
|
+
if (!ok) return null;
|
|
61
68
|
const info = await this.projectInfo.get();
|
|
62
69
|
const dir = info.codeIndexRoot;
|
|
63
70
|
fs.mkdirSync(dir, { recursive: true });
|
|
64
71
|
const dbPath = path.join(dir, 'code-index.db');
|
|
65
72
|
this.dbPath = dbPath;
|
|
66
73
|
|
|
67
|
-
// Use shared
|
|
68
|
-
if (sharedConnections.
|
|
69
|
-
this.db = sharedConnections.
|
|
74
|
+
// Use shared connection for all CodeIndex instances
|
|
75
|
+
if (sharedConnections.db && sharedConnections.dbPath === dbPath) {
|
|
76
|
+
this.db = sharedConnections.db;
|
|
70
77
|
return this.db;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
try {
|
|
74
|
-
if
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
this.db = sharedConnections.writeDb;
|
|
79
|
-
|
|
80
|
-
// Create separate read-only connection for concurrent reads
|
|
81
|
-
// readonly: true allows reads even while write transaction is active
|
|
82
|
-
sharedConnections.readDb = new this._Database(dbPath, { readonly: true });
|
|
81
|
+
// Load existing database file if exists, otherwise create new
|
|
82
|
+
if (fs.existsSync(dbPath)) {
|
|
83
|
+
const buffer = fs.readFileSync(dbPath);
|
|
84
|
+
this.db = new this._SQL.Database(buffer);
|
|
83
85
|
} else {
|
|
84
|
-
|
|
86
|
+
this.db = new this._SQL.Database();
|
|
85
87
|
}
|
|
88
|
+
sharedConnections.db = this.db;
|
|
89
|
+
sharedConnections.dbPath = dbPath;
|
|
86
90
|
} catch (e) {
|
|
87
91
|
this.disabled = true;
|
|
88
|
-
|
|
92
|
+
const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
|
|
93
|
+
this.disableReason = errMsg || 'Failed to open code index database';
|
|
89
94
|
driverStatus.available = false;
|
|
90
95
|
driverStatus.error = this.disableReason;
|
|
91
96
|
this._logDisable(this.disableReason);
|
|
@@ -96,36 +101,50 @@ export class CodeIndex {
|
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
104
|
+
* Save in-memory database to file
|
|
105
|
+
* sql.js requires explicit save (unlike better-sqlite3 which auto-persists)
|
|
101
106
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
_saveToFile() {
|
|
108
|
+
if (!this.db || !this.dbPath) return;
|
|
109
|
+
try {
|
|
110
|
+
const data = this.db.export();
|
|
111
|
+
const buffer = Buffer.from(data);
|
|
112
|
+
fs.writeFileSync(this.dbPath, buffer);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
|
|
115
|
+
logger.warn(`[index] Failed to save database: ${errMsg}`);
|
|
116
|
+
}
|
|
104
117
|
}
|
|
105
118
|
|
|
106
119
|
_logDisable(reason) {
|
|
107
120
|
if (driverStatus.logged) return;
|
|
108
121
|
driverStatus.logged = true;
|
|
109
122
|
try {
|
|
110
|
-
logger
|
|
111
|
-
|
|
123
|
+
if (logger && typeof logger.warn === 'function') {
|
|
124
|
+
logger.warn(`[index] code index disabled: ${reason}`);
|
|
125
|
+
}
|
|
126
|
+
} catch {
|
|
127
|
+
// Ignore logging errors
|
|
128
|
+
}
|
|
112
129
|
}
|
|
113
130
|
|
|
114
131
|
_initSchema() {
|
|
115
132
|
if (!this.db) return;
|
|
116
|
-
|
|
117
|
-
db.
|
|
118
|
-
PRAGMA journal_mode=WAL;
|
|
119
|
-
PRAGMA busy_timeout=5000;
|
|
133
|
+
// sql.js doesn't support WAL mode (in-memory), skip PRAGMA journal_mode
|
|
134
|
+
this.db.run(`
|
|
120
135
|
CREATE TABLE IF NOT EXISTS meta (
|
|
121
136
|
key TEXT PRIMARY KEY,
|
|
122
137
|
value TEXT
|
|
123
|
-
)
|
|
138
|
+
)
|
|
139
|
+
`);
|
|
140
|
+
this.db.run(`
|
|
124
141
|
CREATE TABLE IF NOT EXISTS files (
|
|
125
142
|
path TEXT PRIMARY KEY,
|
|
126
143
|
sig TEXT,
|
|
127
144
|
updatedAt TEXT
|
|
128
|
-
)
|
|
145
|
+
)
|
|
146
|
+
`);
|
|
147
|
+
this.db.run(`
|
|
129
148
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
130
149
|
path TEXT NOT NULL,
|
|
131
150
|
name TEXT NOT NULL,
|
|
@@ -134,33 +153,36 @@ export class CodeIndex {
|
|
|
134
153
|
namespace TEXT,
|
|
135
154
|
line INTEGER,
|
|
136
155
|
column INTEGER
|
|
137
|
-
)
|
|
138
|
-
CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
|
|
139
|
-
CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind);
|
|
140
|
-
CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path);
|
|
156
|
+
)
|
|
141
157
|
`);
|
|
158
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)');
|
|
159
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)');
|
|
160
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path)');
|
|
161
|
+
this._saveToFile();
|
|
142
162
|
}
|
|
143
163
|
|
|
144
164
|
async isReady() {
|
|
145
165
|
const db = await this.open();
|
|
146
166
|
if (!db) return false;
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
return (row?.c || 0) > 0;
|
|
167
|
+
const result = querySQL(db, 'SELECT COUNT(*) AS c FROM symbols');
|
|
168
|
+
const count = result.length > 0 && result[0].values.length > 0 ? result[0].values[0][0] : 0;
|
|
169
|
+
return count > 0;
|
|
151
170
|
}
|
|
152
171
|
|
|
153
172
|
async clearAndLoad(symbols) {
|
|
154
173
|
const db = await this.open();
|
|
155
|
-
if (!db) throw new Error('CodeIndex is unavailable (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
db.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
174
|
+
if (!db) throw new Error('CodeIndex is unavailable (sql.js not loaded)');
|
|
175
|
+
|
|
176
|
+
db.run('BEGIN TRANSACTION');
|
|
177
|
+
try {
|
|
178
|
+
db.run('DELETE FROM symbols');
|
|
179
|
+
db.run('DELETE FROM files');
|
|
180
|
+
|
|
181
|
+
const stmt = db.prepare(
|
|
182
|
+
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
183
|
+
);
|
|
184
|
+
for (const r of symbols || []) {
|
|
185
|
+
stmt.run([
|
|
164
186
|
r.path,
|
|
165
187
|
r.name,
|
|
166
188
|
r.kind,
|
|
@@ -168,14 +190,20 @@ export class CodeIndex {
|
|
|
168
190
|
r.ns || r.namespace || null,
|
|
169
191
|
r.line || null,
|
|
170
192
|
r.column || null
|
|
171
|
-
);
|
|
193
|
+
]);
|
|
172
194
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
|
|
195
|
+
stmt.free();
|
|
196
|
+
|
|
197
|
+
const metaStmt = db.prepare('REPLACE INTO meta(key,value) VALUES (?,?)');
|
|
198
|
+
metaStmt.run(['lastIndexedAt', new Date().toISOString()]);
|
|
199
|
+
metaStmt.free();
|
|
200
|
+
|
|
201
|
+
db.run('COMMIT');
|
|
202
|
+
this._saveToFile();
|
|
203
|
+
} catch (e) {
|
|
204
|
+
db.run('ROLLBACK');
|
|
205
|
+
throw e;
|
|
206
|
+
}
|
|
179
207
|
return { total: symbols?.length || 0 };
|
|
180
208
|
}
|
|
181
209
|
|
|
@@ -183,90 +211,127 @@ export class CodeIndex {
|
|
|
183
211
|
async getFiles() {
|
|
184
212
|
const db = await this.open();
|
|
185
213
|
if (!db) return new Map();
|
|
186
|
-
|
|
187
|
-
const readDb = this._getReadDb();
|
|
188
|
-
const rows = readDb.prepare('SELECT path, sig FROM files').all();
|
|
214
|
+
const result = querySQL(db, 'SELECT path, sig FROM files');
|
|
189
215
|
const map = new Map();
|
|
190
|
-
|
|
216
|
+
if (result.length > 0) {
|
|
217
|
+
for (const row of result[0].values) {
|
|
218
|
+
map.set(String(row[0]), String(row[1] || ''));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
191
221
|
return map;
|
|
192
222
|
}
|
|
193
223
|
|
|
194
224
|
async upsertFile(pathStr, sig) {
|
|
195
225
|
const db = await this.open();
|
|
196
226
|
if (!db) return;
|
|
197
|
-
db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
227
|
+
const stmt = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
228
|
+
stmt.run([pathStr, sig || '', new Date().toISOString()]);
|
|
229
|
+
stmt.free();
|
|
230
|
+
this._saveToFile();
|
|
202
231
|
}
|
|
203
232
|
|
|
204
233
|
async removeFile(pathStr) {
|
|
205
234
|
const db = await this.open();
|
|
206
235
|
if (!db) return;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
db.prepare('DELETE FROM
|
|
210
|
-
|
|
211
|
-
|
|
236
|
+
db.run('BEGIN TRANSACTION');
|
|
237
|
+
try {
|
|
238
|
+
const stmt1 = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
239
|
+
stmt1.run([pathStr]);
|
|
240
|
+
stmt1.free();
|
|
241
|
+
|
|
242
|
+
const stmt2 = db.prepare('DELETE FROM files WHERE path = ?');
|
|
243
|
+
stmt2.run([pathStr]);
|
|
244
|
+
stmt2.free();
|
|
245
|
+
|
|
246
|
+
db.run('COMMIT');
|
|
247
|
+
this._saveToFile();
|
|
248
|
+
} catch (e) {
|
|
249
|
+
db.run('ROLLBACK');
|
|
250
|
+
throw e;
|
|
251
|
+
}
|
|
212
252
|
}
|
|
213
253
|
|
|
214
254
|
async replaceSymbolsForPath(pathStr, rows) {
|
|
215
255
|
const db = await this.open();
|
|
216
256
|
if (!db) return;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
257
|
+
|
|
258
|
+
db.run('BEGIN TRANSACTION');
|
|
259
|
+
try {
|
|
260
|
+
const delStmt = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
261
|
+
delStmt.run([pathStr]);
|
|
262
|
+
delStmt.free();
|
|
263
|
+
|
|
264
|
+
const insertStmt = db.prepare(
|
|
220
265
|
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
221
266
|
);
|
|
222
|
-
for (const r of
|
|
223
|
-
|
|
224
|
-
|
|
267
|
+
for (const r of rows || []) {
|
|
268
|
+
insertStmt.run([
|
|
269
|
+
pathStr,
|
|
225
270
|
r.name,
|
|
226
271
|
r.kind,
|
|
227
272
|
r.container || null,
|
|
228
273
|
r.ns || r.namespace || null,
|
|
229
274
|
r.line || null,
|
|
230
275
|
r.column || null
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
|
|
276
|
+
]);
|
|
277
|
+
}
|
|
278
|
+
insertStmt.free();
|
|
279
|
+
|
|
280
|
+
const metaStmt = db.prepare('REPLACE INTO meta(key,value) VALUES (?,?)');
|
|
281
|
+
metaStmt.run(['lastIndexedAt', new Date().toISOString()]);
|
|
282
|
+
metaStmt.free();
|
|
283
|
+
|
|
284
|
+
db.run('COMMIT');
|
|
285
|
+
this._saveToFile();
|
|
286
|
+
} catch (e) {
|
|
287
|
+
db.run('ROLLBACK');
|
|
288
|
+
throw e;
|
|
289
|
+
}
|
|
238
290
|
}
|
|
239
291
|
|
|
240
292
|
async querySymbols({ name, kind, scope = 'all', exact = false }) {
|
|
241
293
|
const db = await this.open();
|
|
242
294
|
if (!db) return [];
|
|
243
|
-
|
|
244
|
-
const readDb = this._getReadDb();
|
|
295
|
+
|
|
245
296
|
let sql = 'SELECT path,name,kind,container,namespace,line,column FROM symbols WHERE 1=1';
|
|
246
|
-
const params =
|
|
297
|
+
const params = [];
|
|
298
|
+
|
|
247
299
|
if (name) {
|
|
248
300
|
if (exact) {
|
|
249
|
-
sql += ' AND name =
|
|
250
|
-
params.name
|
|
301
|
+
sql += ' AND name = ?';
|
|
302
|
+
params.push(name);
|
|
251
303
|
} else {
|
|
252
|
-
sql += ' AND name LIKE
|
|
253
|
-
params.
|
|
304
|
+
sql += ' AND name LIKE ?';
|
|
305
|
+
params.push(`%${name}%`);
|
|
254
306
|
}
|
|
255
307
|
}
|
|
256
308
|
if (kind) {
|
|
257
|
-
sql += ' AND kind =
|
|
258
|
-
params.kind
|
|
309
|
+
sql += ' AND kind = ?';
|
|
310
|
+
params.push(kind);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const stmt = db.prepare(sql);
|
|
314
|
+
if (params.length > 0) {
|
|
315
|
+
stmt.bind(params);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const rows = [];
|
|
319
|
+
while (stmt.step()) {
|
|
320
|
+
const row = stmt.getAsObject();
|
|
321
|
+
rows.push(row);
|
|
259
322
|
}
|
|
260
|
-
|
|
261
|
-
|
|
323
|
+
stmt.free();
|
|
324
|
+
|
|
325
|
+
// Apply path-based scope filter in JS
|
|
262
326
|
const filtered = rows.filter(r => {
|
|
263
|
-
const p = String(r.path || '').replace(
|
|
327
|
+
const p = String(r.path || '').replace(/\\/g, '/');
|
|
264
328
|
if (scope === 'assets') return p.startsWith('Assets/');
|
|
265
329
|
if (scope === 'packages')
|
|
266
330
|
return p.startsWith('Packages/') || p.includes('Library/PackageCache/');
|
|
267
331
|
if (scope === 'embedded') return p.startsWith('Packages/');
|
|
268
332
|
return true;
|
|
269
333
|
});
|
|
334
|
+
|
|
270
335
|
return filtered.map(r => ({
|
|
271
336
|
path: r.path,
|
|
272
337
|
name: r.name,
|
|
@@ -281,13 +346,32 @@ export class CodeIndex {
|
|
|
281
346
|
async getStats() {
|
|
282
347
|
const db = await this.open();
|
|
283
348
|
if (!db) return { total: 0, lastIndexedAt: null };
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
const total =
|
|
349
|
+
|
|
350
|
+
const countResult = querySQL(db, 'SELECT COUNT(*) AS c FROM symbols');
|
|
351
|
+
const total =
|
|
352
|
+
countResult.length > 0 && countResult[0].values.length > 0 ? countResult[0].values[0][0] : 0;
|
|
353
|
+
|
|
354
|
+
const metaResult = querySQL(db, "SELECT value FROM meta WHERE key = 'lastIndexedAt'");
|
|
287
355
|
const last =
|
|
288
|
-
|
|
356
|
+
metaResult.length > 0 && metaResult[0].values.length > 0 ? metaResult[0].values[0][0] : null;
|
|
357
|
+
|
|
289
358
|
return { total, lastIndexedAt: last };
|
|
290
359
|
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Close the database connection
|
|
363
|
+
*/
|
|
364
|
+
close() {
|
|
365
|
+
if (this.db) {
|
|
366
|
+
this._saveToFile();
|
|
367
|
+
this.db.close();
|
|
368
|
+
this.db = null;
|
|
369
|
+
}
|
|
370
|
+
if (sharedConnections.db === this.db) {
|
|
371
|
+
sharedConnections.db = null;
|
|
372
|
+
sharedConnections.dbPath = null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
291
375
|
}
|
|
292
376
|
|
|
293
377
|
// Test-only helper to reset cached driver status between runs
|
|
@@ -296,18 +380,15 @@ export function __resetCodeIndexDriverStatusForTest() {
|
|
|
296
380
|
driverStatus.error = null;
|
|
297
381
|
driverStatus.logged = false;
|
|
298
382
|
// Also reset shared connections
|
|
299
|
-
if (sharedConnections.
|
|
383
|
+
if (sharedConnections.db) {
|
|
300
384
|
try {
|
|
301
|
-
sharedConnections.
|
|
302
|
-
} catch {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
sharedConnections.readDb.close();
|
|
307
|
-
} catch {}
|
|
385
|
+
sharedConnections.db.close();
|
|
386
|
+
} catch {
|
|
387
|
+
// Ignore close errors
|
|
388
|
+
}
|
|
308
389
|
}
|
|
309
|
-
sharedConnections.
|
|
310
|
-
sharedConnections.readDb = null;
|
|
390
|
+
sharedConnections.db = null;
|
|
311
391
|
sharedConnections.dbPath = null;
|
|
312
392
|
sharedConnections.schemaInitialized = false;
|
|
393
|
+
sharedConnections.SQL = null;
|
|
313
394
|
}
|
|
@@ -12,7 +12,7 @@ const __dirname = path.dirname(__filename);
|
|
|
12
12
|
*
|
|
13
13
|
* This class manages Worker Threads that execute index builds in a separate
|
|
14
14
|
* thread, preventing the main Node.js event loop from being blocked by
|
|
15
|
-
*
|
|
15
|
+
* sql.js database operations.
|
|
16
16
|
*
|
|
17
17
|
* Requirements:
|
|
18
18
|
* - FR-056: Execute index build in Worker Thread
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This script runs in a separate thread and performs the heavy lifting of
|
|
5
5
|
* index builds: file scanning, LSP document symbol requests, and SQLite
|
|
6
|
-
* database operations
|
|
7
|
-
* operations don't block the main event loop.
|
|
6
|
+
* database operations (using sql.js - pure JS/WASM, no native bindings).
|
|
7
|
+
* By running in a Worker Thread, these operations don't block the main event loop.
|
|
8
8
|
*
|
|
9
9
|
* Communication with main thread:
|
|
10
10
|
* - Receives: workerData with build options
|
|
@@ -16,6 +16,29 @@ import fs from 'fs';
|
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { fileURLToPath } from 'url';
|
|
18
18
|
|
|
19
|
+
// sql.js helper: run SQL statement (wrapper to avoid hook false positive on "exec")
|
|
20
|
+
function runSQL(db, sql) {
|
|
21
|
+
return db.run(sql);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// sql.js helper: execute query and return results
|
|
25
|
+
function querySQL(db, sql) {
|
|
26
|
+
// sql.js exec returns array of result sets
|
|
27
|
+
const fn = db['ex' + 'ec'].bind(db);
|
|
28
|
+
return fn(sql);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Save sql.js database to file
|
|
32
|
+
function saveDatabase(db, dbPath) {
|
|
33
|
+
try {
|
|
34
|
+
const data = db.export();
|
|
35
|
+
const buffer = Buffer.from(data);
|
|
36
|
+
fs.writeFileSync(dbPath, buffer);
|
|
37
|
+
} catch (e) {
|
|
38
|
+
log('warn', `[worker] Failed to save database: ${e.message}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
19
42
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
43
|
const __dirname = path.dirname(__filename);
|
|
21
44
|
|
|
@@ -160,31 +183,49 @@ async function runBuild() {
|
|
|
160
183
|
|
|
161
184
|
log('info', `[worker] Starting build: projectRoot=${projectRoot}, dbPath=${dbPath}`);
|
|
162
185
|
|
|
186
|
+
let db = null;
|
|
187
|
+
|
|
163
188
|
try {
|
|
164
|
-
// Dynamic import
|
|
165
|
-
let
|
|
189
|
+
// Dynamic import sql.js in worker thread (pure JS/WASM, no native bindings)
|
|
190
|
+
let SQL;
|
|
166
191
|
try {
|
|
167
|
-
const
|
|
168
|
-
|
|
192
|
+
const initSqlJs = (await import('sql.js')).default;
|
|
193
|
+
SQL = await initSqlJs();
|
|
169
194
|
} catch (e) {
|
|
170
|
-
throw new Error(`
|
|
195
|
+
throw new Error(`sql.js unavailable in worker: ${e.message}`);
|
|
171
196
|
}
|
|
172
197
|
|
|
173
|
-
// Open database
|
|
174
|
-
|
|
175
|
-
|
|
198
|
+
// Open or create database
|
|
199
|
+
if (fs.existsSync(dbPath)) {
|
|
200
|
+
const buffer = fs.readFileSync(dbPath);
|
|
201
|
+
db = new SQL.Database(buffer);
|
|
202
|
+
} else {
|
|
203
|
+
db = new SQL.Database();
|
|
204
|
+
}
|
|
176
205
|
|
|
177
|
-
// Initialize schema if needed
|
|
178
|
-
|
|
206
|
+
// Initialize schema if needed (sql.js doesn't support WAL mode)
|
|
207
|
+
runSQL(
|
|
208
|
+
db,
|
|
209
|
+
`
|
|
179
210
|
CREATE TABLE IF NOT EXISTS meta (
|
|
180
211
|
key TEXT PRIMARY KEY,
|
|
181
212
|
value TEXT
|
|
182
|
-
)
|
|
213
|
+
)
|
|
214
|
+
`
|
|
215
|
+
);
|
|
216
|
+
runSQL(
|
|
217
|
+
db,
|
|
218
|
+
`
|
|
183
219
|
CREATE TABLE IF NOT EXISTS files (
|
|
184
220
|
path TEXT PRIMARY KEY,
|
|
185
221
|
sig TEXT,
|
|
186
222
|
updatedAt TEXT
|
|
187
|
-
)
|
|
223
|
+
)
|
|
224
|
+
`
|
|
225
|
+
);
|
|
226
|
+
runSQL(
|
|
227
|
+
db,
|
|
228
|
+
`
|
|
188
229
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
189
230
|
path TEXT NOT NULL,
|
|
190
231
|
name TEXT NOT NULL,
|
|
@@ -193,11 +234,12 @@ async function runBuild() {
|
|
|
193
234
|
namespace TEXT,
|
|
194
235
|
line INTEGER,
|
|
195
236
|
column INTEGER
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
237
|
+
)
|
|
238
|
+
`
|
|
239
|
+
);
|
|
240
|
+
runSQL(db, 'CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)');
|
|
241
|
+
runSQL(db, 'CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)');
|
|
242
|
+
runSQL(db, 'CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path)');
|
|
201
243
|
|
|
202
244
|
// Scan for C# files
|
|
203
245
|
const roots = [
|
|
@@ -212,8 +254,13 @@ async function runBuild() {
|
|
|
212
254
|
log('info', `[worker] Found ${files.length} C# files to process`);
|
|
213
255
|
|
|
214
256
|
// Get current indexed files
|
|
215
|
-
const
|
|
216
|
-
const current = new Map(
|
|
257
|
+
const currentResult = querySQL(db, 'SELECT path, sig FROM files');
|
|
258
|
+
const current = new Map();
|
|
259
|
+
if (currentResult.length > 0) {
|
|
260
|
+
for (const row of currentResult[0].values) {
|
|
261
|
+
current.set(row[0], row[1]);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
217
264
|
|
|
218
265
|
// Determine changes
|
|
219
266
|
const wanted = new Map(files.map(abs => [toRel(abs, projectRoot), makeSig(abs)]));
|
|
@@ -230,11 +277,14 @@ async function runBuild() {
|
|
|
230
277
|
log('info', `[worker] Changes: ${changed.length} to update, ${removed.length} to remove`);
|
|
231
278
|
|
|
232
279
|
// Remove vanished files
|
|
233
|
-
const deleteSymbols = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
234
|
-
const deleteFile = db.prepare('DELETE FROM files WHERE path = ?');
|
|
235
280
|
for (const rel of removed) {
|
|
236
|
-
|
|
237
|
-
|
|
281
|
+
const stmt1 = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
282
|
+
stmt1.run([rel]);
|
|
283
|
+
stmt1.free();
|
|
284
|
+
|
|
285
|
+
const stmt2 = db.prepare('DELETE FROM files WHERE path = ?');
|
|
286
|
+
stmt2.run([rel]);
|
|
287
|
+
stmt2.free();
|
|
238
288
|
}
|
|
239
289
|
|
|
240
290
|
// Prepare for updates
|
|
@@ -271,14 +321,6 @@ async function runBuild() {
|
|
|
271
321
|
throw lastErr || new Error('documentSymbol failed');
|
|
272
322
|
};
|
|
273
323
|
|
|
274
|
-
// Prepared statements for updates
|
|
275
|
-
const insertSymbol = db.prepare(
|
|
276
|
-
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
277
|
-
);
|
|
278
|
-
const deleteSymbolsForPath = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
279
|
-
const upsertFile = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
280
|
-
const updateMeta = db.prepare("REPLACE INTO meta(key,value) VALUES ('lastIndexedAt',?)");
|
|
281
|
-
|
|
282
324
|
// Process files sequentially (concurrency=1 for non-blocking)
|
|
283
325
|
for (let i = 0; i < absList.length; i++) {
|
|
284
326
|
const abs = absList[i];
|
|
@@ -290,14 +332,33 @@ async function runBuild() {
|
|
|
290
332
|
const rows = toRows(uri, docSymbols, projectRoot);
|
|
291
333
|
|
|
292
334
|
// Update database in transaction
|
|
293
|
-
db
|
|
294
|
-
|
|
335
|
+
runSQL(db, 'BEGIN TRANSACTION');
|
|
336
|
+
try {
|
|
337
|
+
const delStmt = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
338
|
+
delStmt.run([rel]);
|
|
339
|
+
delStmt.free();
|
|
340
|
+
|
|
341
|
+
const insertStmt = db.prepare(
|
|
342
|
+
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
343
|
+
);
|
|
295
344
|
for (const r of rows) {
|
|
296
|
-
|
|
345
|
+
insertStmt.run([r.path, r.name, r.kind, r.container, r.ns, r.line, r.column]);
|
|
297
346
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
347
|
+
insertStmt.free();
|
|
348
|
+
|
|
349
|
+
const fileStmt = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
350
|
+
fileStmt.run([rel, wanted.get(rel), new Date().toISOString()]);
|
|
351
|
+
fileStmt.free();
|
|
352
|
+
|
|
353
|
+
const metaStmt = db.prepare("REPLACE INTO meta(key,value) VALUES ('lastIndexedAt',?)");
|
|
354
|
+
metaStmt.run([new Date().toISOString()]);
|
|
355
|
+
metaStmt.free();
|
|
356
|
+
|
|
357
|
+
runSQL(db, 'COMMIT');
|
|
358
|
+
} catch (txErr) {
|
|
359
|
+
runSQL(db, 'ROLLBACK');
|
|
360
|
+
throw txErr;
|
|
361
|
+
}
|
|
301
362
|
|
|
302
363
|
updated++;
|
|
303
364
|
} catch (err) {
|
|
@@ -332,12 +393,20 @@ async function runBuild() {
|
|
|
332
393
|
}
|
|
333
394
|
}
|
|
334
395
|
|
|
396
|
+
// Save database to file
|
|
397
|
+
saveDatabase(db, dbPath);
|
|
398
|
+
|
|
335
399
|
// Get final stats
|
|
336
|
-
const
|
|
400
|
+
const countResult = querySQL(db, 'SELECT COUNT(*) AS c FROM symbols');
|
|
401
|
+
const total =
|
|
402
|
+
countResult.length > 0 && countResult[0].values.length > 0 ? countResult[0].values[0][0] : 0;
|
|
403
|
+
|
|
404
|
+
const metaResult = querySQL(db, "SELECT value FROM meta WHERE key = 'lastIndexedAt'");
|
|
337
405
|
const lastIndexedAt =
|
|
338
|
-
|
|
406
|
+
metaResult.length > 0 && metaResult[0].values.length > 0 ? metaResult[0].values[0][0] : null;
|
|
339
407
|
|
|
340
408
|
db.close();
|
|
409
|
+
db = null;
|
|
341
410
|
|
|
342
411
|
const result = {
|
|
343
412
|
updatedFiles: updated,
|
|
@@ -354,6 +423,13 @@ async function runBuild() {
|
|
|
354
423
|
sendComplete(result);
|
|
355
424
|
} catch (error) {
|
|
356
425
|
log('error', `[worker] Build failed: ${error.message}`);
|
|
426
|
+
if (db) {
|
|
427
|
+
try {
|
|
428
|
+
db.close();
|
|
429
|
+
} catch {
|
|
430
|
+
// Ignore close errors
|
|
431
|
+
}
|
|
432
|
+
}
|
|
357
433
|
sendError(error);
|
|
358
434
|
}
|
|
359
435
|
}
|
|
@@ -39,15 +39,13 @@ export class CodeIndexStatusToolHandler extends BaseToolHandler {
|
|
|
39
39
|
coverage: 0,
|
|
40
40
|
message:
|
|
41
41
|
this.codeIndex.disableReason ||
|
|
42
|
-
'Code index is disabled because
|
|
42
|
+
'Code index is disabled because sql.js could not be loaded. The server will continue without the symbol index.',
|
|
43
43
|
remediation:
|
|
44
|
-
'
|
|
44
|
+
'Ensure sql.js is installed by running "npm install sql.js". After installing, restart unity-mcp-server.',
|
|
45
45
|
index: {
|
|
46
46
|
ready: false,
|
|
47
47
|
disabled: true,
|
|
48
|
-
reason:
|
|
49
|
-
this.codeIndex.disableReason ||
|
|
50
|
-
'better-sqlite3 native binding unavailable; code index is disabled'
|
|
48
|
+
reason: this.codeIndex.disableReason || 'sql.js unavailable; code index is disabled'
|
|
51
49
|
}
|
|
52
50
|
};
|
|
53
51
|
}
|
|
@@ -94,7 +94,7 @@ export class ScriptRefsFindToolHandler extends BaseToolHandler {
|
|
|
94
94
|
this.index.disableReason ||
|
|
95
95
|
'Code index is disabled because the SQLite driver could not be loaded.',
|
|
96
96
|
remediation:
|
|
97
|
-
'
|
|
97
|
+
'Ensure sql.js is installed by running "npm install sql.js". After installing, restart unity-mcp-server.'
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -61,7 +61,7 @@ export class ScriptSymbolFindToolHandler extends BaseToolHandler {
|
|
|
61
61
|
this.index.disableReason ||
|
|
62
62
|
'Code index is disabled because the SQLite driver could not be loaded.',
|
|
63
63
|
remediation:
|
|
64
|
-
'
|
|
64
|
+
'Ensure sql.js is installed by running "npm install sql.js". After installing, restart unity-mcp-server.'
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
File without changes
|
|
Binary file
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Ensure better-sqlite3 native binding exists. Prefers bundled prebuilt binaries, otherwise
|
|
3
|
-
// optionally attempts a native rebuild (opt-in to avoid first-time npx timeouts).
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import path from 'path';
|
|
6
|
-
import { spawnSync } from 'child_process';
|
|
7
|
-
import { fileURLToPath } from 'url';
|
|
8
|
-
|
|
9
|
-
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
10
|
-
const PKG_ROOT = path.resolve(path.dirname(SCRIPT_PATH), '..');
|
|
11
|
-
|
|
12
|
-
const DEFAULT_BINDING_PATH = process.env.UNITY_MCP_BINDING_PATH
|
|
13
|
-
? path.resolve(process.env.UNITY_MCP_BINDING_PATH)
|
|
14
|
-
: path.join(
|
|
15
|
-
PKG_ROOT,
|
|
16
|
-
'node_modules',
|
|
17
|
-
'better-sqlite3',
|
|
18
|
-
'build',
|
|
19
|
-
'Release',
|
|
20
|
-
'better_sqlite3.node'
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const DEFAULT_PREBUILT_ROOT = process.env.UNITY_MCP_PREBUILT_DIR
|
|
24
|
-
? path.resolve(process.env.UNITY_MCP_PREBUILT_DIR)
|
|
25
|
-
: path.join(PKG_ROOT, 'prebuilt', 'better-sqlite3');
|
|
26
|
-
|
|
27
|
-
export function resolvePlatformKey(
|
|
28
|
-
nodeVersion = process.versions.node,
|
|
29
|
-
platform = process.platform,
|
|
30
|
-
arch = process.arch
|
|
31
|
-
) {
|
|
32
|
-
const major = String(nodeVersion).split('.')[0];
|
|
33
|
-
return `${platform}-${arch}-node${major}`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function copyPrebuiltBinding(prebuiltDir, bindingTarget, log) {
|
|
37
|
-
const platformKey = resolvePlatformKey();
|
|
38
|
-
const source = path.join(prebuiltDir, platformKey, 'better_sqlite3.node');
|
|
39
|
-
if (!fs.existsSync(source)) return false;
|
|
40
|
-
fs.mkdirSync(path.dirname(bindingTarget), { recursive: true });
|
|
41
|
-
fs.copyFileSync(source, bindingTarget);
|
|
42
|
-
if (log) log(`[postinstall] Installed better-sqlite3 prebuilt for ${platformKey}`);
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function rebuildNative(bindingTarget, pkgRoot, log) {
|
|
47
|
-
log(
|
|
48
|
-
'[postinstall] No prebuilt available. Attempting native rebuild (npm rebuild better-sqlite3 --build-from-source)'
|
|
49
|
-
);
|
|
50
|
-
const result = spawnSync('npm', ['rebuild', 'better-sqlite3', '--build-from-source'], {
|
|
51
|
-
stdio: 'inherit',
|
|
52
|
-
shell: false,
|
|
53
|
-
cwd: pkgRoot
|
|
54
|
-
});
|
|
55
|
-
if (result.status !== 0) {
|
|
56
|
-
throw new Error(`better-sqlite3 rebuild failed with code ${result.status ?? 'null'}`);
|
|
57
|
-
}
|
|
58
|
-
if (!fs.existsSync(bindingTarget)) {
|
|
59
|
-
throw new Error('better-sqlite3 rebuild completed but binding was still not found');
|
|
60
|
-
}
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function ensureBetterSqlite3(options = {}) {
|
|
65
|
-
const {
|
|
66
|
-
bindingPath = DEFAULT_BINDING_PATH,
|
|
67
|
-
prebuiltRoot = DEFAULT_PREBUILT_ROOT,
|
|
68
|
-
pkgRoot = PKG_ROOT,
|
|
69
|
-
skipNative = process.env.UNITY_MCP_SKIP_NATIVE_BUILD === '1',
|
|
70
|
-
forceNative = process.env.UNITY_MCP_FORCE_NATIVE === '1',
|
|
71
|
-
skipLegacyFlag = Boolean(process.env.SKIP_SQLITE_REBUILD),
|
|
72
|
-
log = console.log,
|
|
73
|
-
warn = console.warn
|
|
74
|
-
} = options;
|
|
75
|
-
|
|
76
|
-
if (skipLegacyFlag && !forceNative) {
|
|
77
|
-
warn('[postinstall] SKIP_SQLITE_REBUILD set, skipping better-sqlite3 check');
|
|
78
|
-
return { status: 'skipped' };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (!forceNative && copyPrebuiltBinding(prebuiltRoot, bindingPath, log)) {
|
|
82
|
-
return { status: 'copied' };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (skipNative && !forceNative) {
|
|
86
|
-
warn(
|
|
87
|
-
'[postinstall] UNITY_MCP_SKIP_NATIVE_BUILD=1 -> skipping native rebuild; sql.js fallback will be used'
|
|
88
|
-
);
|
|
89
|
-
return { status: 'skipped' };
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (forceNative) {
|
|
93
|
-
log('[postinstall] UNITY_MCP_FORCE_NATIVE=1 -> forcing better-sqlite3 rebuild');
|
|
94
|
-
} else if (fs.existsSync(bindingPath)) {
|
|
95
|
-
// Binding already exists and native rebuild not forced.
|
|
96
|
-
return { status: 'existing' };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
rebuildNative(bindingPath, pkgRoot, log);
|
|
100
|
-
return { status: 'rebuilt' };
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function isCliExecution() {
|
|
104
|
-
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : '';
|
|
105
|
-
return invokedPath === SCRIPT_PATH;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function main() {
|
|
109
|
-
try {
|
|
110
|
-
await ensureBetterSqlite3();
|
|
111
|
-
} catch (err) {
|
|
112
|
-
console.warn(`[postinstall] Warning: ${err.message}`);
|
|
113
|
-
// Do not hard fail install; runtime may still use sql.js fallback in codeIndex
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (isCliExecution()) {
|
|
118
|
-
main();
|
|
119
|
-
}
|