magector 2.6.0 → 2.6.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magector",
3
- "version": "2.6.0",
3
+ "version": "2.6.2",
4
4
  "description": "Semantic code search for Magento 2 — index, search, MCP server",
5
5
  "type": "module",
6
6
  "main": "src/mcp-server.js",
@@ -33,10 +33,10 @@
33
33
  "ruvector": "^0.1.96"
34
34
  },
35
35
  "optionalDependencies": {
36
- "@magector/cli-darwin-arm64": "2.6.0",
37
- "@magector/cli-linux-x64": "2.6.0",
38
- "@magector/cli-linux-arm64": "2.6.0",
39
- "@magector/cli-win32-x64": "2.6.0"
36
+ "@magector/cli-darwin-arm64": "2.6.2",
37
+ "@magector/cli-linux-x64": "2.6.2",
38
+ "@magector/cli-linux-arm64": "2.6.2",
39
+ "@magector/cli-win32-x64": "2.6.2"
40
40
  },
41
41
  "keywords": [
42
42
  "magento",
package/src/cli.js CHANGED
@@ -12,6 +12,8 @@ import { resolveBinary } from './binary.js';
12
12
  import { ensureModels, resolveModels } from './model.js';
13
13
  import { init, setup } from './init.js';
14
14
  import { checkForUpdate } from './update.js';
15
+ import { createRequire } from 'module';
16
+ const __cliPkg = createRequire(import.meta.url)('../package.json');
15
17
 
16
18
  const args = process.argv.slice(2);
17
19
  const command = args[0];
@@ -327,6 +329,12 @@ async function main() {
327
329
  await import('./validation/benchmark.js');
328
330
  break;
329
331
 
332
+ case 'version':
333
+ case '--version':
334
+ case '-V':
335
+ console.log(`magector v${__cliPkg.version}`);
336
+ break;
337
+
330
338
  case 'help':
331
339
  case '--help':
332
340
  case '-h':
package/src/mcp-server.js CHANGED
@@ -202,7 +202,16 @@ function releasePrimaryLock() {
202
202
  * Write the serve process PID to disk so future instances can clean up orphans.
203
203
  */
204
204
  function writePidFile(pid) {
205
- try { writeFileSync(PID_PATH, String(pid)); } catch {}
205
+ try { writeFileSync(PID_PATH, `${pid}\n${__pkg.version}`); } catch {}
206
+ }
207
+
208
+ function getServePidVersion() {
209
+ try {
210
+ if (!existsSync(PID_PATH)) return null;
211
+ const content = readFileSync(PID_PATH, 'utf-8').trim();
212
+ const lines = content.split('\n');
213
+ return lines[1] || null;
214
+ } catch { return null; }
206
215
  }
207
216
 
208
217
  function removePidFile() {
@@ -809,20 +818,29 @@ async function rustSearchAsync(query, limit = 10) {
809
818
  if (queryFn) {
810
819
  try {
811
820
  const resp = await queryFn('search', { query, limit });
812
- if (resp.ok && Array.isArray(resp.data)) {
821
+ if (resp.ok && Array.isArray(resp.data) && resp.data.length > 0) {
813
822
  cacheSet(cacheKey, resp.data);
814
823
  return resp.data;
815
824
  }
825
+ // Serve returned empty results — fall through to execFileSync
826
+ // This catches stale serve processes with wrong/empty index
827
+ if (resp.ok && Array.isArray(resp.data) && resp.data.length === 0) {
828
+ logToFile('WARN', `Serve returned 0 results for "${query}" — trying execFileSync fallback`);
829
+ }
816
830
  } catch (err) {
817
831
  logToFile('WARN', `Serve query failed, falling back to execFileSync: ${err.message}`);
818
832
  }
819
833
  }
820
834
 
821
- // Fallback: cold-start execFileSync
835
+ // Fallback: cold-start execFileSync (always works if CLI works)
822
836
  logToFile('INFO', `Using execFileSync fallback for search: "${query}"`);
823
837
  try {
824
838
  const result = rustSearchSync(query, limit);
825
- return Array.isArray(result) ? result : [];
839
+ const arr = Array.isArray(result) ? result : [];
840
+ if (arr.length > 0) {
841
+ cacheSet(cacheKey, arr);
842
+ }
843
+ return arr;
826
844
  } catch (err) {
827
845
  logToFile('WARN', `execFileSync fallback failed: ${err.message}`);
828
846
  return [];
@@ -2261,7 +2279,20 @@ async function analyzeImpact(className) {
2261
2279
  // Use vector search to find candidate files (much faster than globbing all PHP)
2262
2280
  const rawSearch = await rustSearchAsync(`${shortName} ${className}`, 50).catch(() => []);
2263
2281
  const raw = Array.isArray(rawSearch) ? rawSearch : [];
2264
- const relatedPaths = raw.map(r => normalizeResult(r)).filter(r => r.path);
2282
+ let relatedPaths = raw.map(r => normalizeResult(r)).filter(r => r.path);
2283
+
2284
+ // Filesystem fallback: if vector search found too few files, find the class file via glob
2285
+ if (relatedPaths.length < 5 && root) {
2286
+ try {
2287
+ const classFiles = await glob(`**/${shortName}.php`, { cwd: root, absolute: false, nodir: true });
2288
+ const existingPaths = new Set(relatedPaths.map(r => r.path));
2289
+ for (const f of classFiles) {
2290
+ if (!existingPaths.has(f)) {
2291
+ relatedPaths.push({ path: f, className: shortName, score: 0.3 });
2292
+ }
2293
+ }
2294
+ } catch {}
2295
+ }
2265
2296
 
2266
2297
  // Check DI references via xml parsing
2267
2298
  const diTrace = await traceDependency(className, 'both');
@@ -4057,10 +4088,49 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4057
4088
  const query = `${args.className} ${ns}`.trim();
4058
4089
  const raw = await rustSearchAsync(query, 30);
4059
4090
  const classLower = args.className.toLowerCase();
4060
- const results = raw.map(normalizeResult).filter(r =>
4091
+ let results = raw.map(normalizeResult).filter(r =>
4061
4092
  r.className?.toLowerCase().includes(classLower) ||
4062
4093
  r.path?.toLowerCase().includes(classLower.replace(/\\/g, '/'))
4063
4094
  );
4095
+
4096
+ // Filesystem fallback: if vector search found nothing, glob for ClassName.php
4097
+ if (results.length === 0 && config.magentoRoot) {
4098
+ const shortName = args.className.split('\\').pop();
4099
+ const globPattern = `**/${shortName}.php`;
4100
+ try {
4101
+ const files = await glob(globPattern, { cwd: config.magentoRoot, absolute: false, nodir: true, ignore: ['**/test/**', '**/tests/**', '**/Test/**'] });
4102
+ // Filter by namespace if provided
4103
+ const nsLower = ns.toLowerCase().replace(/\\\\/g, '/').replace(/\\/g, '/');
4104
+ const matched = files.filter(f => {
4105
+ if (!nsLower) return true;
4106
+ return f.toLowerCase().includes(nsLower);
4107
+ }).slice(0, 10);
4108
+ // Build result entries from file paths
4109
+ for (const filePath of matched) {
4110
+ const absPath = path.join(config.magentoRoot, filePath);
4111
+ let className = shortName;
4112
+ try {
4113
+ const content = readFileSync(absPath, 'utf-8');
4114
+ const nsMatch = content.match(/namespace\s+([\w\\]+)/);
4115
+ if (nsMatch) className = nsMatch[1] + '\\' + shortName;
4116
+ const methodsFound = [];
4117
+ const methodRegex = /public\s+function\s+(\w+)\s*\(/g;
4118
+ let mm;
4119
+ while ((mm = methodRegex.exec(content)) !== null) methodsFound.push(mm[1]);
4120
+ results.push({
4121
+ path: filePath,
4122
+ className,
4123
+ methods: methodsFound,
4124
+ score: 0.5,
4125
+ searchText: content.slice(0, 300)
4126
+ });
4127
+ } catch {
4128
+ results.push({ path: filePath, className, score: 0.5 });
4129
+ }
4130
+ }
4131
+ } catch {}
4132
+ }
4133
+
4064
4134
  return {
4065
4135
  content: [{
4066
4136
  type: 'text',
@@ -4209,14 +4279,20 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4209
4279
  const diFiles = await getDiXmlFiles(fpRoot);
4210
4280
  // Normalize target class for matching (both \ and \\)
4211
4281
  const normalizedTarget = args.targetClass.replace(/\\\\/g, '\\');
4282
+ const isFqcn = normalizedTarget.includes('\\');
4283
+ const shortTarget = normalizedTarget.split('\\').pop().toLowerCase();
4212
4284
  for (const { content, relPath } of diFiles) {
4213
- if (!content.includes(normalizedTarget)) continue;
4285
+ if (!content.includes(isFqcn ? normalizedTarget : args.targetClass)) continue;
4214
4286
  // Find plugin registrations for this target
4215
4287
  const typeBlockRegex = /<type\s+name="([^"]+)"[^>]*>([\s\S]*?)<\/type>/g;
4216
4288
  let tm;
4217
4289
  while ((tm = typeBlockRegex.exec(content)) !== null) {
4218
4290
  const typeName = tm[1].replace(/\\\\/g, '\\');
4219
- if (typeName !== normalizedTarget) continue;
4291
+ // FQCN: exact match. Short name: match if type ends with the short name
4292
+ const typeMatches = isFqcn
4293
+ ? typeName === normalizedTarget
4294
+ : typeName.split('\\').pop().toLowerCase() === shortTarget;
4295
+ if (!typeMatches) continue;
4220
4296
  const block = tm[2];
4221
4297
  const pluginRegex = /<plugin\s+([^/>]*)\/?>/g;
4222
4298
  let pm;
@@ -4288,19 +4364,35 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4288
4364
  }
4289
4365
 
4290
4366
  case 'magento_find_observer': {
4291
- const query = `event ${args.eventName} observer`;
4292
- const raw = await rustSearchAsync(query, 30);
4293
- let results = raw.map(normalizeResult).filter(r =>
4294
- r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml')
4295
- );
4296
- results = rerank(results, { isObserver: true, pathContains: ['events.xml', '/Observer/'] });
4367
+ // Primary: parse events.xml for exact event name match (structural, not semantic)
4368
+ const eventFlow = await traceEventFlow(args.eventName);
4369
+ let text = '';
4370
+
4371
+ if (eventFlow.observers.length > 0) {
4372
+ text += `### Observers for \`${args.eventName}\` (${eventFlow.observers.length})\n\n`;
4373
+ for (const obs of eventFlow.observers) {
4374
+ text += `- **${obs.name}** → \`${obs.instance}::${obs.method}()\` (${obs.file})\n`;
4375
+ }
4376
+ if (eventFlow.observerDetails.length > 0) {
4377
+ text += `\n### Observer PHP Files\n`;
4378
+ for (const det of eventFlow.observerDetails) {
4379
+ text += `- \`${det.instance}\` → ${det.path}\n`;
4380
+ }
4381
+ }
4382
+ }
4297
4383
 
4298
- return {
4299
- content: [{
4300
- type: 'text',
4301
- text: formatSearchResults(results.slice(0, 15))
4302
- }]
4303
- };
4384
+ // Fallback: semantic search if events.xml parsing found nothing
4385
+ if (eventFlow.observers.length === 0) {
4386
+ const query = `event ${args.eventName} observer`;
4387
+ const raw = await rustSearchAsync(query, 30);
4388
+ let results = raw.map(normalizeResult).filter(r =>
4389
+ r.isObserver || r.path?.includes('/Observer/') || r.path?.includes('events.xml')
4390
+ );
4391
+ results = rerank(results, { isObserver: true, pathContains: ['events.xml', '/Observer/'] });
4392
+ text = formatSearchResults(results.slice(0, 15));
4393
+ }
4394
+
4395
+ return { content: [{ type: 'text', text }] };
4304
4396
  }
4305
4397
 
4306
4398
  case 'magento_find_preference': {
@@ -4592,18 +4684,40 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
4592
4684
  // Support both app/code (Magento/Catalog/) and vendor (module-catalog/) paths
4593
4685
  const modulePath = args.moduleName.replace('_', '/') + '/';
4594
4686
  const parts = args.moduleName.split('_');
4687
+ // Hyphenate camelCase for vendor path: OrderSplit → order-split
4595
4688
  const vendorPath = parts.length === 2
4596
- ? `module-${parts[1].toLowerCase()}/`
4689
+ ? `module-${parts[1].replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()}/`
4597
4690
  : '';
4598
- const results = raw.map(normalizeResult).filter(r => {
4599
- const path = r.path || '';
4691
+ let results = raw.map(normalizeResult).filter(r => {
4692
+ const p = r.path || '';
4600
4693
  const mod = r.module || '';
4601
4694
  // Exact module match or directory-level path match (trailing slash prevents Catalog matching CatalogRule)
4602
4695
  return mod === args.moduleName ||
4603
- path.includes(modulePath) ||
4604
- (vendorPath && path.toLowerCase().includes(vendorPath));
4696
+ p.includes(modulePath) ||
4697
+ (vendorPath && p.toLowerCase().includes(vendorPath));
4605
4698
  });
4606
4699
 
4700
+ // Filesystem fallback: if vector search found nothing, glob the module directory
4701
+ if (results.length === 0 && config.magentoRoot && vendorPath) {
4702
+ try {
4703
+ const vendorGlob = `**/${vendorPath}**/*.{php,xml,phtml}`;
4704
+ const files = await glob(vendorGlob, { cwd: config.magentoRoot, absolute: false, nodir: true });
4705
+ for (const f of files.slice(0, 100)) {
4706
+ const entry = { path: f, score: 0.5 };
4707
+ if (f.includes('/Controller/')) entry.isController = true;
4708
+ if (f.includes('/Model/')) entry.isModel = true;
4709
+ if (f.includes('/Block/')) entry.isBlock = true;
4710
+ if (f.includes('/Plugin/')) entry.isPlugin = true;
4711
+ if (f.includes('/Observer/')) entry.isObserver = true;
4712
+ if (f.endsWith('.xml')) entry.type = 'xml';
4713
+ // Extract class name from path
4714
+ const phpMatch = f.match(/\/([A-Z]\w+)\.php$/);
4715
+ if (phpMatch) entry.className = phpMatch[1];
4716
+ results.push(entry);
4717
+ }
4718
+ } catch {}
4719
+ }
4720
+
4607
4721
  const structure = {
4608
4722
  controllers: results.filter(r => r.isController || r.path?.includes('/Controller/')),
4609
4723
  models: results.filter(r => r.isModel || (r.path?.includes('/Model/') && !r.path?.includes('ResourceModel'))),
@@ -5450,9 +5564,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5450
5564
  const qr = `${a.className} ${ns}`.trim();
5451
5565
  const raw = await rustSearchAsync(qr, 30);
5452
5566
  const cl = a.className.toLowerCase();
5453
- const res = raw.map(normalizeResult).filter(r =>
5567
+ let res = raw.map(normalizeResult).filter(r =>
5454
5568
  r.className?.toLowerCase().includes(cl) || r.path?.toLowerCase().includes(cl.replace(/\\/g, '/'))
5455
5569
  );
5570
+ // Filesystem fallback for batch find_class
5571
+ if (res.length === 0 && config.magentoRoot) {
5572
+ const shortName = a.className.split('\\').pop();
5573
+ try {
5574
+ const files = await glob(`**/${shortName}.php`, { cwd: config.magentoRoot, absolute: false, nodir: true });
5575
+ for (const f of files.slice(0, 5)) {
5576
+ res.push({ path: f, className: shortName, score: 0.5 });
5577
+ }
5578
+ } catch {}
5579
+ }
5456
5580
  text = formatSearchResults(res.slice(0, 5));
5457
5581
  break;
5458
5582
  }
@@ -5533,6 +5657,34 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
5533
5657
  text = formatSearchResults(res.slice(0, 5));
5534
5658
  break;
5535
5659
  }
5660
+ case 'magento_module_structure': {
5661
+ const raw = await rustSearchAsync(a.moduleName, 200);
5662
+ const modulePath = a.moduleName.replace('_', '/') + '/';
5663
+ const parts = a.moduleName.split('_');
5664
+ const vendorPath = parts.length === 2 ? `module-${parts[1].toLowerCase()}/` : '';
5665
+ const res = raw.map(normalizeResult).filter(r => {
5666
+ const p = r.path || '';
5667
+ const mod = r.module || '';
5668
+ return mod === a.moduleName || p.includes(modulePath) || (vendorPath && p.toLowerCase().includes(vendorPath));
5669
+ });
5670
+ text = `Module: ${a.moduleName} (${res.length} files)\n`;
5671
+ const cats = { controllers: '/Controller/', models: '/Model/', plugins: '/Plugin/', observers: '/Observer/', api: '/Api/' };
5672
+ for (const [cat, pattern] of Object.entries(cats)) {
5673
+ const matches = res.filter(r => r.path?.includes(pattern));
5674
+ if (matches.length > 0) {
5675
+ text += `${cat}: ${matches.length} (${matches.slice(0, 3).map(r => r.className || r.path?.split('/').pop()).join(', ')})\n`;
5676
+ }
5677
+ }
5678
+ break;
5679
+ }
5680
+ case 'magento_find_observer': {
5681
+ const flow = await traceEventFlow(a.eventName);
5682
+ text = `Observers: ${flow.observers.length}\n`;
5683
+ for (const o of flow.observers.slice(0, 10)) {
5684
+ text += `- ${o.name}: ${o.instance}::${o.method}() (${o.file})\n`;
5685
+ }
5686
+ break;
5687
+ }
5536
5688
  default:
5537
5689
  text = `Unsupported batch tool: ${q.tool}`;
5538
5690
  }
@@ -5625,6 +5777,16 @@ async function main() {
5625
5777
  try {
5626
5778
  let role = 'secondary';
5627
5779
 
5780
+ // Kill stale serve process if version mismatch (e.g., user upgraded Magector)
5781
+ const staleVersion = getServePidVersion();
5782
+ if (staleVersion && staleVersion !== __pkg.version) {
5783
+ logToFile('WARN', `Serve process version mismatch: ${staleVersion} vs ${__pkg.version} — killing stale process`);
5784
+ console.error(`Killing stale serve process (version ${staleVersion}, current ${__pkg.version})`);
5785
+ killStaleServeProcess();
5786
+ // Remove stale socket so we don't connect to it
5787
+ try { if (existsSync(SOCK_PATH)) unlinkSync(SOCK_PATH); } catch {}
5788
+ }
5789
+
5628
5790
  const connected = await tryConnectSocket();
5629
5791
  if (connected) {
5630
5792
  logToFile('INFO', 'Joined existing serve process via socket (secondary)');