@akiojin/unity-mcp-server 2.44.0 → 2.45.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 +27 -58
- package/package.json +6 -10
- package/src/core/codeIndex.js +196 -116
- package/src/core/indexBuildWorkerPool.js +1 -1
- package/src/core/workers/indexBuildWorker.js +115 -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,50 @@ 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
|
+
| **fast-sql** | Code index database | npm dependency | Hybrid backend: better-sqlite3 (native) or sql.js (WASM) |
|
|
319
|
+
| **csharp-lsp** | C# symbol analysis | Downloaded on first use | ~80 MB per platform |
|
|
320
320
|
|
|
321
|
-
#### Why
|
|
321
|
+
#### Why fast-sql?
|
|
322
322
|
|
|
323
|
-
We
|
|
323
|
+
We use **fast-sql** (hybrid SQLite library) for the code index database:
|
|
324
324
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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
|
+
- **Optimal performance**: Uses better-sqlite3 (native) when available for ~34x faster queries
|
|
326
|
+
- **npx compatible**: Falls back to sql.js (WASM) when native bindings unavailable
|
|
327
|
+
- **Zero native compilation required**: Works immediately via `npx` without build tools
|
|
328
|
+
- **Cross-platform**: Automatic backend selection based on environment
|
|
332
329
|
|
|
333
|
-
|
|
330
|
+
**Performance comparison** (better-sqlite3 vs sql.js):
|
|
331
|
+
- 50K inserts: 29ms vs 53ms (1.8x faster)
|
|
332
|
+
- 1000 queries: 0.34μs vs 11.78μs (34x faster)
|
|
333
|
+
- LIKE search: 2.5ms vs 7ms (2.8x faster)
|
|
334
334
|
|
|
335
|
-
|
|
335
|
+
#### csharp-lsp Distribution
|
|
336
336
|
|
|
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)**:
|
|
337
|
+
**csharp-lsp (downloaded on first use)**:
|
|
346
338
|
|
|
347
339
|
- Large size (~80 MB per platform, ~480 MB for all 6 platforms)
|
|
348
|
-
- Too large to bundle in npm package
|
|
340
|
+
- Too large to bundle in npm package
|
|
349
341
|
- Downloaded from GitHub Release on first use of script editing tools
|
|
350
342
|
|
|
351
343
|
#### Supported Platforms
|
|
352
344
|
|
|
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
|
|
345
|
+
| Platform | fast-sql | csharp-lsp |
|
|
346
|
+
|----------|----------|------------|
|
|
347
|
+
| Linux x64 | ✅ Native (better-sqlite3) | ✅ Downloaded (~79 MB) |
|
|
348
|
+
| Linux arm64 | ✅ Native (better-sqlite3) | ✅ Downloaded (~86 MB) |
|
|
349
|
+
| macOS x64 | ✅ Native (better-sqlite3) | ✅ Downloaded (~80 MB) |
|
|
350
|
+
| macOS arm64 (Apple Silicon) | ✅ Native (better-sqlite3) | ✅ Downloaded (~86 MB) |
|
|
351
|
+
| Windows x64 | ✅ Native (better-sqlite3) | ✅ Downloaded (~80 MB) |
|
|
352
|
+
| Windows arm64 | ✅ WASM fallback (sql.js) | ✅ Downloaded (~85 MB) |
|
|
366
353
|
|
|
367
354
|
#### Storage Locations
|
|
368
355
|
|
|
369
|
-
- **better-sqlite3**: `<package>/prebuilt/better-sqlite3/<platform>/`
|
|
370
356
|
- **csharp-lsp**: `~/.unity/tools/csharp-lsp/<rid>/`
|
|
371
357
|
- **Code index database**: `<workspace>/.unity/cache/code-index/code-index.db`
|
|
372
358
|
|
|
@@ -422,23 +408,6 @@ npm uninstall -g @akiojin/unity-mcp-server
|
|
|
422
408
|
npm install -g @akiojin/unity-mcp-server
|
|
423
409
|
```
|
|
424
410
|
|
|
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
411
|
### MCP Client Shows "Capabilities: none"
|
|
443
412
|
|
|
444
413
|
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.
|
|
3
|
+
"version": "2.45.0",
|
|
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
|
+
"@akiojin/fast-sql": "file:../packages/fast-sql"
|
|
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,11 @@ import path from 'path';
|
|
|
3
3
|
import { ProjectInfoProvider } from './projectInfo.js';
|
|
4
4
|
import { logger } from './config.js';
|
|
5
5
|
|
|
6
|
+
// fast-sql helper: execute query and return results
|
|
7
|
+
function querySQL(db, sql) {
|
|
8
|
+
return db.execSql(sql);
|
|
9
|
+
}
|
|
10
|
+
|
|
6
11
|
// Shared driver availability state across CodeIndex instances
|
|
7
12
|
const driverStatus = {
|
|
8
13
|
available: null,
|
|
@@ -12,21 +17,21 @@ const driverStatus = {
|
|
|
12
17
|
|
|
13
18
|
// Shared DB connections (singleton pattern for concurrent access)
|
|
14
19
|
const sharedConnections = {
|
|
15
|
-
|
|
16
|
-
readDb: null,
|
|
20
|
+
db: null,
|
|
17
21
|
dbPath: null,
|
|
18
|
-
schemaInitialized: false
|
|
22
|
+
schemaInitialized: false,
|
|
23
|
+
SQL: null // sql.js factory
|
|
19
24
|
};
|
|
20
25
|
|
|
21
26
|
export class CodeIndex {
|
|
22
27
|
constructor(unityConnection) {
|
|
23
28
|
this.unityConnection = unityConnection;
|
|
24
29
|
this.projectInfo = new ProjectInfoProvider(unityConnection);
|
|
25
|
-
this.db = null;
|
|
30
|
+
this.db = null;
|
|
26
31
|
this.dbPath = null;
|
|
27
|
-
this.disabled = false;
|
|
32
|
+
this.disabled = false;
|
|
28
33
|
this.disableReason = null;
|
|
29
|
-
this.
|
|
34
|
+
this._SQL = null;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
37
|
async _ensureDriver() {
|
|
@@ -35,18 +40,19 @@ export class CodeIndex {
|
|
|
35
40
|
this.disableReason = this.disableReason || driverStatus.error;
|
|
36
41
|
return false;
|
|
37
42
|
}
|
|
38
|
-
if (this.
|
|
43
|
+
if (this._SQL) return true;
|
|
39
44
|
try {
|
|
40
|
-
// Dynamic import
|
|
41
|
-
const
|
|
42
|
-
this.
|
|
45
|
+
// Dynamic import fast-sql (hybrid backend: better-sqlite3 or sql.js fallback)
|
|
46
|
+
const initFastSql = (await import('@akiojin/fast-sql')).default;
|
|
47
|
+
this._SQL = await initFastSql();
|
|
48
|
+
sharedConnections.SQL = this._SQL;
|
|
43
49
|
driverStatus.available = true;
|
|
44
50
|
driverStatus.error = null;
|
|
45
51
|
return true;
|
|
46
52
|
} catch (e) {
|
|
47
|
-
// No fallback - fail fast with clear error
|
|
48
53
|
this.disabled = true;
|
|
49
|
-
|
|
54
|
+
const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
|
|
55
|
+
this.disableReason = `fast-sql unavailable: ${errMsg}. Code index features are disabled.`;
|
|
50
56
|
driverStatus.available = false;
|
|
51
57
|
driverStatus.error = this.disableReason;
|
|
52
58
|
this._logDisable(this.disableReason);
|
|
@@ -57,35 +63,33 @@ export class CodeIndex {
|
|
|
57
63
|
async open() {
|
|
58
64
|
if (this.db) return this.db;
|
|
59
65
|
const ok = await this._ensureDriver();
|
|
60
|
-
if (!ok) return null;
|
|
66
|
+
if (!ok) return null;
|
|
61
67
|
const info = await this.projectInfo.get();
|
|
62
68
|
const dir = info.codeIndexRoot;
|
|
63
69
|
fs.mkdirSync(dir, { recursive: true });
|
|
64
70
|
const dbPath = path.join(dir, 'code-index.db');
|
|
65
71
|
this.dbPath = dbPath;
|
|
66
72
|
|
|
67
|
-
// Use shared
|
|
68
|
-
if (sharedConnections.
|
|
69
|
-
this.db = sharedConnections.
|
|
73
|
+
// Use shared connection for all CodeIndex instances
|
|
74
|
+
if (sharedConnections.db && sharedConnections.dbPath === dbPath) {
|
|
75
|
+
this.db = sharedConnections.db;
|
|
70
76
|
return this.db;
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
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 });
|
|
80
|
+
// Load existing database file if exists, otherwise create new
|
|
81
|
+
if (fs.existsSync(dbPath)) {
|
|
82
|
+
const buffer = fs.readFileSync(dbPath);
|
|
83
|
+
this.db = new this._SQL.Database(buffer);
|
|
83
84
|
} else {
|
|
84
|
-
|
|
85
|
+
this.db = new this._SQL.Database();
|
|
85
86
|
}
|
|
87
|
+
sharedConnections.db = this.db;
|
|
88
|
+
sharedConnections.dbPath = dbPath;
|
|
86
89
|
} catch (e) {
|
|
87
90
|
this.disabled = true;
|
|
88
|
-
|
|
91
|
+
const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
|
|
92
|
+
this.disableReason = errMsg || 'Failed to open code index database';
|
|
89
93
|
driverStatus.available = false;
|
|
90
94
|
driverStatus.error = this.disableReason;
|
|
91
95
|
this._logDisable(this.disableReason);
|
|
@@ -96,36 +100,50 @@ export class CodeIndex {
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
103
|
+
* Save in-memory database to file
|
|
104
|
+
* fast-sql requires explicit save when using sql.js backend
|
|
101
105
|
*/
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
_saveToFile() {
|
|
107
|
+
if (!this.db || !this.dbPath) return;
|
|
108
|
+
try {
|
|
109
|
+
const data = this.db.exportDb();
|
|
110
|
+
const buffer = Buffer.from(data);
|
|
111
|
+
fs.writeFileSync(this.dbPath, buffer);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
const errMsg = e && typeof e === 'object' && 'message' in e ? e.message : String(e);
|
|
114
|
+
logger.warn(`[index] Failed to save database: ${errMsg}`);
|
|
115
|
+
}
|
|
104
116
|
}
|
|
105
117
|
|
|
106
118
|
_logDisable(reason) {
|
|
107
119
|
if (driverStatus.logged) return;
|
|
108
120
|
driverStatus.logged = true;
|
|
109
121
|
try {
|
|
110
|
-
logger
|
|
111
|
-
|
|
122
|
+
if (logger && typeof logger.warn === 'function') {
|
|
123
|
+
logger.warn(`[index] code index disabled: ${reason}`);
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// Ignore logging errors
|
|
127
|
+
}
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
_initSchema() {
|
|
115
131
|
if (!this.db) return;
|
|
116
|
-
|
|
117
|
-
db.
|
|
118
|
-
PRAGMA journal_mode=WAL;
|
|
119
|
-
PRAGMA busy_timeout=5000;
|
|
132
|
+
// fast-sql applies optimal PRAGMAs automatically
|
|
133
|
+
this.db.run(`
|
|
120
134
|
CREATE TABLE IF NOT EXISTS meta (
|
|
121
135
|
key TEXT PRIMARY KEY,
|
|
122
136
|
value TEXT
|
|
123
|
-
)
|
|
137
|
+
)
|
|
138
|
+
`);
|
|
139
|
+
this.db.run(`
|
|
124
140
|
CREATE TABLE IF NOT EXISTS files (
|
|
125
141
|
path TEXT PRIMARY KEY,
|
|
126
142
|
sig TEXT,
|
|
127
143
|
updatedAt TEXT
|
|
128
|
-
)
|
|
144
|
+
)
|
|
145
|
+
`);
|
|
146
|
+
this.db.run(`
|
|
129
147
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
130
148
|
path TEXT NOT NULL,
|
|
131
149
|
name TEXT NOT NULL,
|
|
@@ -134,33 +152,36 @@ export class CodeIndex {
|
|
|
134
152
|
namespace TEXT,
|
|
135
153
|
line INTEGER,
|
|
136
154
|
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);
|
|
155
|
+
)
|
|
141
156
|
`);
|
|
157
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)');
|
|
158
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)');
|
|
159
|
+
this.db.run('CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path)');
|
|
160
|
+
this._saveToFile();
|
|
142
161
|
}
|
|
143
162
|
|
|
144
163
|
async isReady() {
|
|
145
164
|
const db = await this.open();
|
|
146
165
|
if (!db) return false;
|
|
147
|
-
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
return (row?.c || 0) > 0;
|
|
166
|
+
const result = querySQL(db, 'SELECT COUNT(*) AS c FROM symbols');
|
|
167
|
+
const count = result.length > 0 && result[0].values.length > 0 ? result[0].values[0][0] : 0;
|
|
168
|
+
return count > 0;
|
|
151
169
|
}
|
|
152
170
|
|
|
153
171
|
async clearAndLoad(symbols) {
|
|
154
172
|
const db = await this.open();
|
|
155
|
-
if (!db) throw new Error('CodeIndex is unavailable (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
db.
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
173
|
+
if (!db) throw new Error('CodeIndex is unavailable (fast-sql not loaded)');
|
|
174
|
+
|
|
175
|
+
db.run('BEGIN TRANSACTION');
|
|
176
|
+
try {
|
|
177
|
+
db.run('DELETE FROM symbols');
|
|
178
|
+
db.run('DELETE FROM files');
|
|
179
|
+
|
|
180
|
+
const stmt = db.prepare(
|
|
181
|
+
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
182
|
+
);
|
|
183
|
+
for (const r of symbols || []) {
|
|
184
|
+
stmt.run([
|
|
164
185
|
r.path,
|
|
165
186
|
r.name,
|
|
166
187
|
r.kind,
|
|
@@ -168,14 +189,20 @@ export class CodeIndex {
|
|
|
168
189
|
r.ns || r.namespace || null,
|
|
169
190
|
r.line || null,
|
|
170
191
|
r.column || null
|
|
171
|
-
);
|
|
192
|
+
]);
|
|
172
193
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
|
|
194
|
+
stmt.free();
|
|
195
|
+
|
|
196
|
+
const metaStmt = db.prepare('REPLACE INTO meta(key,value) VALUES (?,?)');
|
|
197
|
+
metaStmt.run(['lastIndexedAt', new Date().toISOString()]);
|
|
198
|
+
metaStmt.free();
|
|
199
|
+
|
|
200
|
+
db.run('COMMIT');
|
|
201
|
+
this._saveToFile();
|
|
202
|
+
} catch (e) {
|
|
203
|
+
db.run('ROLLBACK');
|
|
204
|
+
throw e;
|
|
205
|
+
}
|
|
179
206
|
return { total: symbols?.length || 0 };
|
|
180
207
|
}
|
|
181
208
|
|
|
@@ -183,90 +210,127 @@ export class CodeIndex {
|
|
|
183
210
|
async getFiles() {
|
|
184
211
|
const db = await this.open();
|
|
185
212
|
if (!db) return new Map();
|
|
186
|
-
|
|
187
|
-
const readDb = this._getReadDb();
|
|
188
|
-
const rows = readDb.prepare('SELECT path, sig FROM files').all();
|
|
213
|
+
const result = querySQL(db, 'SELECT path, sig FROM files');
|
|
189
214
|
const map = new Map();
|
|
190
|
-
|
|
215
|
+
if (result.length > 0) {
|
|
216
|
+
for (const row of result[0].values) {
|
|
217
|
+
map.set(String(row[0]), String(row[1] || ''));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
191
220
|
return map;
|
|
192
221
|
}
|
|
193
222
|
|
|
194
223
|
async upsertFile(pathStr, sig) {
|
|
195
224
|
const db = await this.open();
|
|
196
225
|
if (!db) return;
|
|
197
|
-
db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)')
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
);
|
|
226
|
+
const stmt = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
227
|
+
stmt.run([pathStr, sig || '', new Date().toISOString()]);
|
|
228
|
+
stmt.free();
|
|
229
|
+
this._saveToFile();
|
|
202
230
|
}
|
|
203
231
|
|
|
204
232
|
async removeFile(pathStr) {
|
|
205
233
|
const db = await this.open();
|
|
206
234
|
if (!db) return;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
db.prepare('DELETE FROM
|
|
210
|
-
|
|
211
|
-
|
|
235
|
+
db.run('BEGIN TRANSACTION');
|
|
236
|
+
try {
|
|
237
|
+
const stmt1 = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
238
|
+
stmt1.run([pathStr]);
|
|
239
|
+
stmt1.free();
|
|
240
|
+
|
|
241
|
+
const stmt2 = db.prepare('DELETE FROM files WHERE path = ?');
|
|
242
|
+
stmt2.run([pathStr]);
|
|
243
|
+
stmt2.free();
|
|
244
|
+
|
|
245
|
+
db.run('COMMIT');
|
|
246
|
+
this._saveToFile();
|
|
247
|
+
} catch (e) {
|
|
248
|
+
db.run('ROLLBACK');
|
|
249
|
+
throw e;
|
|
250
|
+
}
|
|
212
251
|
}
|
|
213
252
|
|
|
214
253
|
async replaceSymbolsForPath(pathStr, rows) {
|
|
215
254
|
const db = await this.open();
|
|
216
255
|
if (!db) return;
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
256
|
+
|
|
257
|
+
db.run('BEGIN TRANSACTION');
|
|
258
|
+
try {
|
|
259
|
+
const delStmt = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
260
|
+
delStmt.run([pathStr]);
|
|
261
|
+
delStmt.free();
|
|
262
|
+
|
|
263
|
+
const insertStmt = db.prepare(
|
|
220
264
|
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
221
265
|
);
|
|
222
|
-
for (const r of
|
|
223
|
-
|
|
224
|
-
|
|
266
|
+
for (const r of rows || []) {
|
|
267
|
+
insertStmt.run([
|
|
268
|
+
pathStr,
|
|
225
269
|
r.name,
|
|
226
270
|
r.kind,
|
|
227
271
|
r.container || null,
|
|
228
272
|
r.ns || r.namespace || null,
|
|
229
273
|
r.line || null,
|
|
230
274
|
r.column || null
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
);
|
|
236
|
-
|
|
237
|
-
|
|
275
|
+
]);
|
|
276
|
+
}
|
|
277
|
+
insertStmt.free();
|
|
278
|
+
|
|
279
|
+
const metaStmt = db.prepare('REPLACE INTO meta(key,value) VALUES (?,?)');
|
|
280
|
+
metaStmt.run(['lastIndexedAt', new Date().toISOString()]);
|
|
281
|
+
metaStmt.free();
|
|
282
|
+
|
|
283
|
+
db.run('COMMIT');
|
|
284
|
+
this._saveToFile();
|
|
285
|
+
} catch (e) {
|
|
286
|
+
db.run('ROLLBACK');
|
|
287
|
+
throw e;
|
|
288
|
+
}
|
|
238
289
|
}
|
|
239
290
|
|
|
240
291
|
async querySymbols({ name, kind, scope = 'all', exact = false }) {
|
|
241
292
|
const db = await this.open();
|
|
242
293
|
if (!db) return [];
|
|
243
|
-
|
|
244
|
-
const readDb = this._getReadDb();
|
|
294
|
+
|
|
245
295
|
let sql = 'SELECT path,name,kind,container,namespace,line,column FROM symbols WHERE 1=1';
|
|
246
|
-
const params =
|
|
296
|
+
const params = [];
|
|
297
|
+
|
|
247
298
|
if (name) {
|
|
248
299
|
if (exact) {
|
|
249
|
-
sql += ' AND name =
|
|
250
|
-
params.name
|
|
300
|
+
sql += ' AND name = ?';
|
|
301
|
+
params.push(name);
|
|
251
302
|
} else {
|
|
252
|
-
sql += ' AND name LIKE
|
|
253
|
-
params.
|
|
303
|
+
sql += ' AND name LIKE ?';
|
|
304
|
+
params.push(`%${name}%`);
|
|
254
305
|
}
|
|
255
306
|
}
|
|
256
307
|
if (kind) {
|
|
257
|
-
sql += ' AND kind =
|
|
258
|
-
params.kind
|
|
308
|
+
sql += ' AND kind = ?';
|
|
309
|
+
params.push(kind);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const stmt = db.prepare(sql);
|
|
313
|
+
if (params.length > 0) {
|
|
314
|
+
stmt.bind(params);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const rows = [];
|
|
318
|
+
while (stmt.step()) {
|
|
319
|
+
const row = stmt.getAsObject();
|
|
320
|
+
rows.push(row);
|
|
259
321
|
}
|
|
260
|
-
|
|
261
|
-
|
|
322
|
+
stmt.free();
|
|
323
|
+
|
|
324
|
+
// Apply path-based scope filter in JS
|
|
262
325
|
const filtered = rows.filter(r => {
|
|
263
|
-
const p = String(r.path || '').replace(
|
|
326
|
+
const p = String(r.path || '').replace(/\\/g, '/');
|
|
264
327
|
if (scope === 'assets') return p.startsWith('Assets/');
|
|
265
328
|
if (scope === 'packages')
|
|
266
329
|
return p.startsWith('Packages/') || p.includes('Library/PackageCache/');
|
|
267
330
|
if (scope === 'embedded') return p.startsWith('Packages/');
|
|
268
331
|
return true;
|
|
269
332
|
});
|
|
333
|
+
|
|
270
334
|
return filtered.map(r => ({
|
|
271
335
|
path: r.path,
|
|
272
336
|
name: r.name,
|
|
@@ -281,13 +345,32 @@ export class CodeIndex {
|
|
|
281
345
|
async getStats() {
|
|
282
346
|
const db = await this.open();
|
|
283
347
|
if (!db) return { total: 0, lastIndexedAt: null };
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
const total =
|
|
348
|
+
|
|
349
|
+
const countResult = querySQL(db, 'SELECT COUNT(*) AS c FROM symbols');
|
|
350
|
+
const total =
|
|
351
|
+
countResult.length > 0 && countResult[0].values.length > 0 ? countResult[0].values[0][0] : 0;
|
|
352
|
+
|
|
353
|
+
const metaResult = querySQL(db, "SELECT value FROM meta WHERE key = 'lastIndexedAt'");
|
|
287
354
|
const last =
|
|
288
|
-
|
|
355
|
+
metaResult.length > 0 && metaResult[0].values.length > 0 ? metaResult[0].values[0][0] : null;
|
|
356
|
+
|
|
289
357
|
return { total, lastIndexedAt: last };
|
|
290
358
|
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Close the database connection
|
|
362
|
+
*/
|
|
363
|
+
close() {
|
|
364
|
+
if (this.db) {
|
|
365
|
+
this._saveToFile();
|
|
366
|
+
this.db.close();
|
|
367
|
+
this.db = null;
|
|
368
|
+
}
|
|
369
|
+
if (sharedConnections.db === this.db) {
|
|
370
|
+
sharedConnections.db = null;
|
|
371
|
+
sharedConnections.dbPath = null;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
291
374
|
}
|
|
292
375
|
|
|
293
376
|
// Test-only helper to reset cached driver status between runs
|
|
@@ -296,18 +379,15 @@ export function __resetCodeIndexDriverStatusForTest() {
|
|
|
296
379
|
driverStatus.error = null;
|
|
297
380
|
driverStatus.logged = false;
|
|
298
381
|
// Also reset shared connections
|
|
299
|
-
if (sharedConnections.
|
|
382
|
+
if (sharedConnections.db) {
|
|
300
383
|
try {
|
|
301
|
-
sharedConnections.
|
|
302
|
-
} catch {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
sharedConnections.readDb.close();
|
|
307
|
-
} catch {}
|
|
384
|
+
sharedConnections.db.close();
|
|
385
|
+
} catch {
|
|
386
|
+
// Ignore close errors
|
|
387
|
+
}
|
|
308
388
|
}
|
|
309
|
-
sharedConnections.
|
|
310
|
-
sharedConnections.readDb = null;
|
|
389
|
+
sharedConnections.db = null;
|
|
311
390
|
sharedConnections.dbPath = null;
|
|
312
391
|
sharedConnections.schemaInitialized = false;
|
|
392
|
+
sharedConnections.SQL = null;
|
|
313
393
|
}
|
|
@@ -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
|
-
* better-sqlite3
|
|
15
|
+
* fast-sql database operations (hybrid backend with better-sqlite3 or sql.js fallback).
|
|
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 fast-sql - hybrid backend with better-sqlite3 or sql.js fallback).
|
|
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,27 @@ import fs from 'fs';
|
|
|
16
16
|
import path from 'path';
|
|
17
17
|
import { fileURLToPath } from 'url';
|
|
18
18
|
|
|
19
|
+
// fast-sql helper: run SQL statement
|
|
20
|
+
function runSQL(db, sql) {
|
|
21
|
+
return db.run(sql);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// fast-sql helper: execute query and return results
|
|
25
|
+
function querySQL(db, sql) {
|
|
26
|
+
return db.execSql(sql);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Save fast-sql database to file
|
|
30
|
+
function saveDatabase(db, dbPath) {
|
|
31
|
+
try {
|
|
32
|
+
const data = db.exportDb();
|
|
33
|
+
const buffer = Buffer.from(data);
|
|
34
|
+
fs.writeFileSync(dbPath, buffer);
|
|
35
|
+
} catch (e) {
|
|
36
|
+
log('warn', `[worker] Failed to save database: ${e.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
19
40
|
const __filename = fileURLToPath(import.meta.url);
|
|
20
41
|
const __dirname = path.dirname(__filename);
|
|
21
42
|
|
|
@@ -160,31 +181,49 @@ async function runBuild() {
|
|
|
160
181
|
|
|
161
182
|
log('info', `[worker] Starting build: projectRoot=${projectRoot}, dbPath=${dbPath}`);
|
|
162
183
|
|
|
184
|
+
let db = null;
|
|
185
|
+
|
|
163
186
|
try {
|
|
164
|
-
// Dynamic import
|
|
165
|
-
let
|
|
187
|
+
// Dynamic import fast-sql in worker thread (hybrid: better-sqlite3 or sql.js fallback)
|
|
188
|
+
let SQL;
|
|
166
189
|
try {
|
|
167
|
-
const
|
|
168
|
-
|
|
190
|
+
const initFastSql = (await import('@akiojin/fast-sql')).default;
|
|
191
|
+
SQL = await initFastSql();
|
|
169
192
|
} catch (e) {
|
|
170
|
-
throw new Error(`
|
|
193
|
+
throw new Error(`fast-sql unavailable in worker: ${e.message}`);
|
|
171
194
|
}
|
|
172
195
|
|
|
173
|
-
// Open database
|
|
174
|
-
|
|
175
|
-
|
|
196
|
+
// Open or create database
|
|
197
|
+
if (fs.existsSync(dbPath)) {
|
|
198
|
+
const buffer = fs.readFileSync(dbPath);
|
|
199
|
+
db = new SQL.Database(buffer);
|
|
200
|
+
} else {
|
|
201
|
+
db = new SQL.Database();
|
|
202
|
+
}
|
|
176
203
|
|
|
177
|
-
// Initialize schema if needed
|
|
178
|
-
|
|
204
|
+
// Initialize schema if needed (fast-sql applies optimal PRAGMAs automatically)
|
|
205
|
+
runSQL(
|
|
206
|
+
db,
|
|
207
|
+
`
|
|
179
208
|
CREATE TABLE IF NOT EXISTS meta (
|
|
180
209
|
key TEXT PRIMARY KEY,
|
|
181
210
|
value TEXT
|
|
182
|
-
)
|
|
211
|
+
)
|
|
212
|
+
`
|
|
213
|
+
);
|
|
214
|
+
runSQL(
|
|
215
|
+
db,
|
|
216
|
+
`
|
|
183
217
|
CREATE TABLE IF NOT EXISTS files (
|
|
184
218
|
path TEXT PRIMARY KEY,
|
|
185
219
|
sig TEXT,
|
|
186
220
|
updatedAt TEXT
|
|
187
|
-
)
|
|
221
|
+
)
|
|
222
|
+
`
|
|
223
|
+
);
|
|
224
|
+
runSQL(
|
|
225
|
+
db,
|
|
226
|
+
`
|
|
188
227
|
CREATE TABLE IF NOT EXISTS symbols (
|
|
189
228
|
path TEXT NOT NULL,
|
|
190
229
|
name TEXT NOT NULL,
|
|
@@ -193,11 +232,12 @@ async function runBuild() {
|
|
|
193
232
|
namespace TEXT,
|
|
194
233
|
line INTEGER,
|
|
195
234
|
column INTEGER
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
235
|
+
)
|
|
236
|
+
`
|
|
237
|
+
);
|
|
238
|
+
runSQL(db, 'CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)');
|
|
239
|
+
runSQL(db, 'CREATE INDEX IF NOT EXISTS idx_symbols_kind ON symbols(kind)');
|
|
240
|
+
runSQL(db, 'CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path)');
|
|
201
241
|
|
|
202
242
|
// Scan for C# files
|
|
203
243
|
const roots = [
|
|
@@ -212,8 +252,13 @@ async function runBuild() {
|
|
|
212
252
|
log('info', `[worker] Found ${files.length} C# files to process`);
|
|
213
253
|
|
|
214
254
|
// Get current indexed files
|
|
215
|
-
const
|
|
216
|
-
const current = new Map(
|
|
255
|
+
const currentResult = querySQL(db, 'SELECT path, sig FROM files');
|
|
256
|
+
const current = new Map();
|
|
257
|
+
if (currentResult.length > 0) {
|
|
258
|
+
for (const row of currentResult[0].values) {
|
|
259
|
+
current.set(row[0], row[1]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
217
262
|
|
|
218
263
|
// Determine changes
|
|
219
264
|
const wanted = new Map(files.map(abs => [toRel(abs, projectRoot), makeSig(abs)]));
|
|
@@ -230,11 +275,14 @@ async function runBuild() {
|
|
|
230
275
|
log('info', `[worker] Changes: ${changed.length} to update, ${removed.length} to remove`);
|
|
231
276
|
|
|
232
277
|
// Remove vanished files
|
|
233
|
-
const deleteSymbols = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
234
|
-
const deleteFile = db.prepare('DELETE FROM files WHERE path = ?');
|
|
235
278
|
for (const rel of removed) {
|
|
236
|
-
|
|
237
|
-
|
|
279
|
+
const stmt1 = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
280
|
+
stmt1.run([rel]);
|
|
281
|
+
stmt1.free();
|
|
282
|
+
|
|
283
|
+
const stmt2 = db.prepare('DELETE FROM files WHERE path = ?');
|
|
284
|
+
stmt2.run([rel]);
|
|
285
|
+
stmt2.free();
|
|
238
286
|
}
|
|
239
287
|
|
|
240
288
|
// Prepare for updates
|
|
@@ -271,14 +319,6 @@ async function runBuild() {
|
|
|
271
319
|
throw lastErr || new Error('documentSymbol failed');
|
|
272
320
|
};
|
|
273
321
|
|
|
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
322
|
// Process files sequentially (concurrency=1 for non-blocking)
|
|
283
323
|
for (let i = 0; i < absList.length; i++) {
|
|
284
324
|
const abs = absList[i];
|
|
@@ -290,14 +330,33 @@ async function runBuild() {
|
|
|
290
330
|
const rows = toRows(uri, docSymbols, projectRoot);
|
|
291
331
|
|
|
292
332
|
// Update database in transaction
|
|
293
|
-
db
|
|
294
|
-
|
|
333
|
+
runSQL(db, 'BEGIN TRANSACTION');
|
|
334
|
+
try {
|
|
335
|
+
const delStmt = db.prepare('DELETE FROM symbols WHERE path = ?');
|
|
336
|
+
delStmt.run([rel]);
|
|
337
|
+
delStmt.free();
|
|
338
|
+
|
|
339
|
+
const insertStmt = db.prepare(
|
|
340
|
+
'INSERT INTO symbols(path,name,kind,container,namespace,line,column) VALUES (?,?,?,?,?,?,?)'
|
|
341
|
+
);
|
|
295
342
|
for (const r of rows) {
|
|
296
|
-
|
|
343
|
+
insertStmt.run([r.path, r.name, r.kind, r.container, r.ns, r.line, r.column]);
|
|
297
344
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
345
|
+
insertStmt.free();
|
|
346
|
+
|
|
347
|
+
const fileStmt = db.prepare('REPLACE INTO files(path,sig,updatedAt) VALUES (?,?,?)');
|
|
348
|
+
fileStmt.run([rel, wanted.get(rel), new Date().toISOString()]);
|
|
349
|
+
fileStmt.free();
|
|
350
|
+
|
|
351
|
+
const metaStmt = db.prepare("REPLACE INTO meta(key,value) VALUES ('lastIndexedAt',?)");
|
|
352
|
+
metaStmt.run([new Date().toISOString()]);
|
|
353
|
+
metaStmt.free();
|
|
354
|
+
|
|
355
|
+
runSQL(db, 'COMMIT');
|
|
356
|
+
} catch (txErr) {
|
|
357
|
+
runSQL(db, 'ROLLBACK');
|
|
358
|
+
throw txErr;
|
|
359
|
+
}
|
|
301
360
|
|
|
302
361
|
updated++;
|
|
303
362
|
} catch (err) {
|
|
@@ -332,12 +391,20 @@ async function runBuild() {
|
|
|
332
391
|
}
|
|
333
392
|
}
|
|
334
393
|
|
|
394
|
+
// Save database to file
|
|
395
|
+
saveDatabase(db, dbPath);
|
|
396
|
+
|
|
335
397
|
// Get final stats
|
|
336
|
-
const
|
|
398
|
+
const countResult = querySQL(db, 'SELECT COUNT(*) AS c FROM symbols');
|
|
399
|
+
const total =
|
|
400
|
+
countResult.length > 0 && countResult[0].values.length > 0 ? countResult[0].values[0][0] : 0;
|
|
401
|
+
|
|
402
|
+
const metaResult = querySQL(db, "SELECT value FROM meta WHERE key = 'lastIndexedAt'");
|
|
337
403
|
const lastIndexedAt =
|
|
338
|
-
|
|
404
|
+
metaResult.length > 0 && metaResult[0].values.length > 0 ? metaResult[0].values[0][0] : null;
|
|
339
405
|
|
|
340
406
|
db.close();
|
|
407
|
+
db = null;
|
|
341
408
|
|
|
342
409
|
const result = {
|
|
343
410
|
updatedFiles: updated,
|
|
@@ -354,6 +421,13 @@ async function runBuild() {
|
|
|
354
421
|
sendComplete(result);
|
|
355
422
|
} catch (error) {
|
|
356
423
|
log('error', `[worker] Build failed: ${error.message}`);
|
|
424
|
+
if (db) {
|
|
425
|
+
try {
|
|
426
|
+
db.close();
|
|
427
|
+
} catch {
|
|
428
|
+
// Ignore close errors
|
|
429
|
+
}
|
|
430
|
+
}
|
|
357
431
|
sendError(error);
|
|
358
432
|
}
|
|
359
433
|
}
|
|
@@ -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 fast-sql could not be loaded. The server will continue without the symbol index.',
|
|
43
43
|
remediation:
|
|
44
|
-
'
|
|
44
|
+
'Ensure fast-sql is properly installed. After reinstalling dependencies, 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 || 'fast-sql 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 fast-sql is properly installed. After reinstalling dependencies, 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 fast-sql is properly installed. After reinstalling dependencies, 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
|
-
}
|