magector 2.2.0 → 2.2.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/package.json +5 -5
- package/src/mcp-server.js +76 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magector",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Semantic code search for Magento 2 — index, search, MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp-server.js",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"ruvector": "^0.1.96"
|
|
40
40
|
},
|
|
41
41
|
"optionalDependencies": {
|
|
42
|
-
"@magector/cli-darwin-arm64": "2.2.
|
|
43
|
-
"@magector/cli-linux-x64": "2.2.
|
|
44
|
-
"@magector/cli-linux-arm64": "2.2.
|
|
45
|
-
"@magector/cli-win32-x64": "2.2.
|
|
42
|
+
"@magector/cli-darwin-arm64": "2.2.1",
|
|
43
|
+
"@magector/cli-linux-x64": "2.2.1",
|
|
44
|
+
"@magector/cli-linux-arm64": "2.2.1",
|
|
45
|
+
"@magector/cli-win32-x64": "2.2.1"
|
|
46
46
|
},
|
|
47
47
|
"keywords": [
|
|
48
48
|
"magento",
|
package/src/mcp-server.js
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { execFileSync, spawn } from 'child_process';
|
|
18
18
|
import { createInterface } from 'readline';
|
|
19
19
|
import { createServer as createNetServer, createConnection } from 'net';
|
|
20
|
-
import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, mkdirSync } from 'fs';
|
|
20
|
+
import { existsSync, statSync, unlinkSync, copyFileSync, renameSync, appendFileSync, writeFileSync, readFileSync, mkdirSync, openSync, closeSync, constants as fsConstants } from 'fs';
|
|
21
21
|
import { stat } from 'fs/promises';
|
|
22
22
|
import { glob } from 'glob';
|
|
23
23
|
import path from 'path';
|
|
@@ -145,6 +145,49 @@ const PID_PATH = path.join(config.magentoRoot, '.magector', 'serve.pid');
|
|
|
145
145
|
const REINDEX_PID_PATH = path.join(config.magentoRoot, '.magector', 'reindex.pid');
|
|
146
146
|
const SOCK_PATH = path.join(config.magentoRoot, '.magector', 'serve.sock');
|
|
147
147
|
const FORMAT_CACHE_PATH = path.join(config.magentoRoot, '.magector', 'format-ok.json');
|
|
148
|
+
const PRIMARY_LOCK_PATH = path.join(config.magentoRoot, '.magector', 'primary.lock');
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Try to acquire the primary lock (O_EXCL = atomic create-or-fail).
|
|
152
|
+
* Returns true if we are the primary instance, false if another instance holds the lock.
|
|
153
|
+
*/
|
|
154
|
+
function tryAcquirePrimaryLock() {
|
|
155
|
+
try {
|
|
156
|
+
const fd = openSync(PRIMARY_LOCK_PATH, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL);
|
|
157
|
+
writeFileSync(fd, String(process.pid));
|
|
158
|
+
closeSync(fd);
|
|
159
|
+
return true;
|
|
160
|
+
} catch {
|
|
161
|
+
// Lock file exists — check if holder is alive
|
|
162
|
+
try {
|
|
163
|
+
const pid = parseInt(readFileSync(PRIMARY_LOCK_PATH, 'utf-8').trim(), 10);
|
|
164
|
+
if (pid && !isNaN(pid)) {
|
|
165
|
+
process.kill(pid, 0); // throws if dead
|
|
166
|
+
return false; // another instance is alive and primary
|
|
167
|
+
}
|
|
168
|
+
} catch { /* holder is dead, take over */ }
|
|
169
|
+
// Stale lock — reclaim
|
|
170
|
+
try { unlinkSync(PRIMARY_LOCK_PATH); } catch {}
|
|
171
|
+
try {
|
|
172
|
+
const fd = openSync(PRIMARY_LOCK_PATH, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL);
|
|
173
|
+
writeFileSync(fd, String(process.pid));
|
|
174
|
+
closeSync(fd);
|
|
175
|
+
return true;
|
|
176
|
+
} catch {
|
|
177
|
+
return false; // another instance beat us
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function releasePrimaryLock() {
|
|
183
|
+
try {
|
|
184
|
+
// Only remove if we own it
|
|
185
|
+
const content = readFileSync(PRIMARY_LOCK_PATH, 'utf-8').trim();
|
|
186
|
+
if (content === String(process.pid)) {
|
|
187
|
+
unlinkSync(PRIMARY_LOCK_PATH);
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
148
191
|
|
|
149
192
|
/**
|
|
150
193
|
* Write the serve process PID to disk so future instances can clean up orphans.
|
|
@@ -3379,15 +3422,22 @@ async function main() {
|
|
|
3379
3422
|
logToFile('INFO', 'Magector MCP server connected (warming up...)');
|
|
3380
3423
|
console.error('Magector MCP server connected (warming up...)');
|
|
3381
3424
|
|
|
3382
|
-
// ── Singleton serve:
|
|
3425
|
+
// ── Singleton serve: one serve process per project ──────────────
|
|
3426
|
+
// 1. Try socket → secondary (instant, no CPU)
|
|
3427
|
+
// 2. Try lock → primary (start serve + socket proxy)
|
|
3428
|
+
// 3. Lock failed → wait for socket (another instance is starting)
|
|
3383
3429
|
try {
|
|
3384
|
-
|
|
3430
|
+
let role = 'secondary';
|
|
3431
|
+
|
|
3385
3432
|
const connected = await tryConnectSocket();
|
|
3386
3433
|
if (connected) {
|
|
3387
|
-
logToFile('INFO', 'Joined existing serve process (
|
|
3388
|
-
console.error('Joined existing serve process (
|
|
3389
|
-
} else {
|
|
3390
|
-
|
|
3434
|
+
logToFile('INFO', 'Joined existing serve process via socket (secondary)');
|
|
3435
|
+
console.error('Joined existing serve process (secondary)');
|
|
3436
|
+
} else if (tryAcquirePrimaryLock()) {
|
|
3437
|
+
role = 'primary';
|
|
3438
|
+
logToFile('INFO', 'Acquired primary lock — this instance owns the serve process');
|
|
3439
|
+
|
|
3440
|
+
// Check DB format (uses cache → instant if already validated)
|
|
3391
3441
|
if (existsSync(config.dbPath)) {
|
|
3392
3442
|
if (!(await checkDbFormat())) {
|
|
3393
3443
|
logToFile('WARN', 'Database format incompatible — scheduling background re-index');
|
|
@@ -3407,12 +3457,11 @@ async function main() {
|
|
|
3407
3457
|
if (serveReadyPromise) {
|
|
3408
3458
|
const ready = await Promise.race([
|
|
3409
3459
|
serveReadyPromise,
|
|
3410
|
-
new Promise(r => setTimeout(() => r(false),
|
|
3460
|
+
new Promise(r => setTimeout(() => r(false), 60000))
|
|
3411
3461
|
]);
|
|
3412
3462
|
if (ready) {
|
|
3413
|
-
logToFile('INFO', 'Serve process ready (primary
|
|
3414
|
-
console.error('Serve process ready (primary
|
|
3415
|
-
// Start socket proxy so other instances can connect
|
|
3463
|
+
logToFile('INFO', 'Serve process ready (primary)');
|
|
3464
|
+
console.error('Serve process ready (primary)');
|
|
3416
3465
|
startSocketProxy();
|
|
3417
3466
|
} else {
|
|
3418
3467
|
logToFile('WARN', 'Serve process not ready in time, will use fallback');
|
|
@@ -3423,6 +3472,21 @@ async function main() {
|
|
|
3423
3472
|
// Non-fatal: falls back to execFileSync per query
|
|
3424
3473
|
}
|
|
3425
3474
|
}
|
|
3475
|
+
} else {
|
|
3476
|
+
// Another instance is starting up — wait for its socket to appear
|
|
3477
|
+
logToFile('INFO', 'Another instance is primary — waiting for socket...');
|
|
3478
|
+
console.error('Waiting for primary instance to start serve process...');
|
|
3479
|
+
for (let i = 0; i < 12; i++) { // wait up to 60s
|
|
3480
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
3481
|
+
if (await tryConnectSocket()) {
|
|
3482
|
+
logToFile('INFO', 'Connected to socket after waiting (secondary)');
|
|
3483
|
+
console.error('Joined existing serve process (secondary)');
|
|
3484
|
+
break;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
if (!globalServeQuery) {
|
|
3488
|
+
logToFile('WARN', 'Socket not available after waiting — using cold-start fallback');
|
|
3489
|
+
}
|
|
3426
3490
|
}
|
|
3427
3491
|
|
|
3428
3492
|
await loadDescriptions();
|
|
@@ -3451,6 +3515,7 @@ function cleanup(reason) {
|
|
|
3451
3515
|
try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
|
|
3452
3516
|
socketServer = null;
|
|
3453
3517
|
}
|
|
3518
|
+
releasePrimaryLock();
|
|
3454
3519
|
removePidFile();
|
|
3455
3520
|
removeReindexPidFile();
|
|
3456
3521
|
}
|