genexus-mcp 3.1.0 → 3.2.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/cli/lib/config.js CHANGED
@@ -56,34 +56,95 @@ function getToolDefinitionsPath() {
56
56
  return candidates[0];
57
57
  }
58
58
 
59
- function discoverGeneXusFromRegistry() {
59
+ function getGeneXusVersionCatalog() {
60
+ const candidates = [
61
+ path.join(__dirname, '..', '..', 'config', 'gx-versions.json'),
62
+ path.join(__dirname, '..', '..', 'publish', 'config', 'gx-versions.json')
63
+ ];
64
+ for (const candidate of candidates) {
65
+ try {
66
+ const catalog = JSON.parse(fs.readFileSync(candidate, 'utf8'));
67
+ if (!catalog || !catalog.primaryMajor || !Array.isArray(catalog.supportedMajors)) continue;
68
+ const entries = catalog.supportedMajors.filter((entry) => entry && /^\d+$/.test(String(entry.major)));
69
+ const primary = entries.find((entry) => String(entry.major) === String(catalog.primaryMajor));
70
+ if (entries.length > 0 && primary?.defaultInstallPath) {
71
+ return { ...catalog, supportedMajors: entries };
72
+ }
73
+ } catch {
74
+ }
75
+ }
76
+ return {
77
+ primaryMajor: '18',
78
+ supportedMajors: [
79
+ { major: '17', displayName: 'GeneXus 17', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXus17Trial' },
80
+ { major: '18', displayName: 'GeneXus 18', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXus18' }
81
+ ],
82
+ source: 'built-in-fallback'
83
+ };
84
+ }
85
+
86
+ function getGeneXusCatalogEntries(preferredMajor = null) {
87
+ const catalog = getGeneXusVersionCatalog();
88
+ return [...catalog.supportedMajors].sort((left, right) => {
89
+ const leftPreferred = preferredMajor !== null && String(left.major) === String(preferredMajor);
90
+ const rightPreferred = preferredMajor !== null && String(right.major) === String(preferredMajor);
91
+ if (leftPreferred !== rightPreferred) return Number(rightPreferred) - Number(leftPreferred);
92
+ const leftPrimary = String(left.major) === String(catalog.primaryMajor);
93
+ const rightPrimary = String(right.major) === String(catalog.primaryMajor);
94
+ return Number(rightPrimary) - Number(leftPrimary);
95
+ });
96
+ }
97
+
98
+ function discoverGeneXusFromRegistry(preferredMajor = null) {
60
99
  if (process.platform !== 'win32') return null;
61
100
  try {
62
101
  const { execFileSync } = require('child_process');
63
- const versions = ['GeneXus 18', 'GeneXus 17', 'GeneXus 16'];
102
+ const entries = getGeneXusCatalogEntries(preferredMajor);
64
103
  const hives = [
65
104
  'HKLM\\SOFTWARE\\WOW6432Node\\Artech',
66
105
  'HKLM\\SOFTWARE\\Artech',
67
106
  'HKCU\\SOFTWARE\\Artech'
68
107
  ];
69
108
  for (const hive of hives) {
70
- for (const ver of versions) {
71
- const key = `${hive}\\${ver}`;
72
- try {
73
- const out = execFileSync('reg.exe', ['query', key, '/v', 'InstallationDirectory'], {
74
- encoding: 'utf8',
75
- stdio: ['ignore', 'pipe', 'ignore'],
76
- windowsHide: true,
77
- timeout: 3000
78
- });
79
- const match = out.match(/InstallationDirectory\s+REG_SZ\s+(.+?)\r?\n/i);
80
- if (match) {
81
- const candidate = match[1].trim().replace(/[\\/]+$/, '');
82
- if (candidate && fs.existsSync(path.join(candidate, 'genexus.exe'))) {
83
- return candidate;
109
+ for (const entry of entries) {
110
+ const names = Array.isArray(entry.registryNames) && entry.registryNames.length > 0
111
+ ? entry.registryNames
112
+ : [`GeneXus ${entry.major}`];
113
+ for (const ver of names) {
114
+ const key = `${hive}\\${ver}`;
115
+ try {
116
+ const out = execFileSync('reg.exe', ['query', key, '/v', 'InstallationDirectory'], {
117
+ encoding: 'utf8',
118
+ stdio: ['ignore', 'pipe', 'ignore'],
119
+ windowsHide: true,
120
+ timeout: 3000
121
+ });
122
+ const match = out.match(/InstallationDirectory\s+REG_SZ\s+(.+?)\r?\n/i);
123
+ if (match) {
124
+ const candidate = match[1].trim().replace(/[\\/]+$/, '');
125
+ if (candidate && fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
126
+ return candidate;
127
+ }
128
+ }
129
+ } catch {
130
+ }
131
+ }
132
+ for (const legacyVersion of (entry.legacyRegistryVersions || [])) {
133
+ const key = `${hive}\\GeneXus\\${legacyVersion}`;
134
+ try {
135
+ const out = execFileSync('reg.exe', ['query', key, '/v', 'InstallPath'], {
136
+ encoding: 'utf8',
137
+ stdio: ['ignore', 'pipe', 'ignore'],
138
+ windowsHide: true,
139
+ timeout: 3000
140
+ });
141
+ const match = out.match(/InstallPath\s+REG_SZ\s+(.+?)\r?\n/i);
142
+ if (match) {
143
+ const candidate = match[1].trim().replace(/[\\/]+$/, '');
144
+ if (candidate && fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
84
145
  }
146
+ } catch {
85
147
  }
86
- } catch {
87
148
  }
88
149
  }
89
150
  }
@@ -92,13 +153,13 @@ function discoverGeneXusFromRegistry() {
92
153
  return null;
93
154
  }
94
155
 
95
- function discoverGeneXusInstallation() {
156
+ function discoverGeneXusInstallation(preferredMajor = null) {
96
157
  if (process.env.GENEXUS_HOME) {
97
158
  const candidate = process.env.GENEXUS_HOME.replace(/[\\/]+$/, '');
98
- if (fs.existsSync(path.join(candidate, 'genexus.exe'))) return candidate;
159
+ if (fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
99
160
  }
100
161
 
101
- const fromRegistry = discoverGeneXusFromRegistry();
162
+ const fromRegistry = discoverGeneXusFromRegistry(preferredMajor);
102
163
  if (fromRegistry) return fromRegistry;
103
164
 
104
165
  const programDirs = [];
@@ -110,17 +171,24 @@ function discoverGeneXusInstallation() {
110
171
  programDirs.push(`${drive}:\\Program Files`);
111
172
  }
112
173
 
113
- const versions = ['GeneXus18', 'GeneXus17', 'GeneXus16'];
174
+ const entries = getGeneXusCatalogEntries(preferredMajor);
114
175
  const seen = new Set();
115
176
  for (const base of programDirs) {
116
177
  const root = path.join(base, 'GeneXus');
117
178
  const key = root.toLowerCase();
118
179
  if (seen.has(key)) continue;
119
180
  seen.add(key);
120
- for (const ver of versions) {
121
- const candidate = path.join(root, ver);
122
- if (fs.existsSync(path.join(candidate, 'genexus.exe'))) {
123
- return candidate;
181
+ for (const entry of entries) {
182
+ const candidateNames = new Set([
183
+ `GeneXus${entry.major}`,
184
+ path.basename(String(entry.defaultInstallPath || ''))
185
+ ]);
186
+ for (const ver of candidateNames) {
187
+ if (!ver) continue;
188
+ const candidate = path.join(root, ver);
189
+ if (fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
190
+ return candidate;
191
+ }
124
192
  }
125
193
  }
126
194
  // Also scan any GeneXus* sibling (e.g. custom-named "GeneXus18 U10").
@@ -128,8 +196,10 @@ function discoverGeneXusInstallation() {
128
196
  if (fs.existsSync(root)) {
129
197
  for (const entry of fs.readdirSync(root)) {
130
198
  if (!/^GeneXus/i.test(entry)) continue;
199
+ const majorMatch = entry.match(/^GeneXus\s*(\d+)/i);
200
+ if (majorMatch && !entries.some((item) => String(item.major) === majorMatch[1])) continue;
131
201
  const candidate = path.join(root, entry);
132
- if (fs.existsSync(path.join(candidate, 'genexus.exe'))) {
202
+ if (fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
133
203
  return candidate;
134
204
  }
135
205
  }
@@ -138,13 +208,13 @@ function discoverGeneXusInstallation() {
138
208
  }
139
209
  }
140
210
 
141
- const fromPath = discoverGeneXusFromPath();
211
+ const fromPath = discoverGeneXusFromPath(preferredMajor);
142
212
  if (fromPath) return fromPath;
143
213
 
144
214
  return null;
145
215
  }
146
216
 
147
- function discoverGeneXusFromPath() {
217
+ function discoverGeneXusFromPath(preferredMajor = null) {
148
218
  if (process.platform !== 'win32') return null;
149
219
  try {
150
220
  const { execFileSync } = require('child_process');
@@ -155,7 +225,7 @@ function discoverGeneXusFromPath() {
155
225
  timeout: 3000
156
226
  });
157
227
  const first = out.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
158
- if (first && fs.existsSync(first)) {
228
+ if (first && fs.existsSync(first) && matchesPreferredGeneXusMajor(path.dirname(first), preferredMajor)) {
159
229
  return path.dirname(first);
160
230
  }
161
231
  } catch {
@@ -1352,8 +1422,43 @@ function getLocalAppDataCacheDir() {
1352
1422
  return path.join(base, 'GenexusMCP');
1353
1423
  }
1354
1424
 
1355
- function readGeneXusVersionFromInstall(gxPath) {
1356
- if (!gxPath) return null;
1425
+ function getGeneXusMajor(version) {
1426
+ const match = String(version || '').match(/^\s*(\d+)/);
1427
+ return match ? match[1] : null;
1428
+ }
1429
+
1430
+ function matchesPreferredGeneXusMajor(gxPath, preferredMajor) {
1431
+ if (preferredMajor === null || preferredMajor === undefined || String(preferredMajor).trim() === '') return true;
1432
+ const identity = readGeneXusInstallationIdentity(gxPath);
1433
+ return String(identity.major || '') === String(preferredMajor);
1434
+ }
1435
+
1436
+ function readExecutableProductVersion(exePath) {
1437
+ if (process.platform !== 'win32' || !exePath || !fs.existsSync(exePath)) return null;
1438
+ try {
1439
+ const { execFileSync } = require('child_process');
1440
+ const output = execFileSync('powershell.exe', [
1441
+ '-NoProfile',
1442
+ '-NonInteractive',
1443
+ '-Command',
1444
+ '$versionInfo = (Get-Item -LiteralPath $env:GXMCP_VERSION_EXE -ErrorAction Stop).VersionInfo; if ($versionInfo.ProductVersion) { $versionInfo.ProductVersion } else { $versionInfo.FileVersion }'
1445
+ ], {
1446
+ encoding: 'utf8',
1447
+ stdio: ['ignore', 'pipe', 'ignore'],
1448
+ windowsHide: true,
1449
+ timeout: 3000,
1450
+ env: { ...process.env, GXMCP_VERSION_EXE: exePath }
1451
+ });
1452
+ return output.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || null;
1453
+ } catch {
1454
+ return null;
1455
+ }
1456
+ }
1457
+
1458
+ function readGeneXusInstallationIdentity(gxPath, options = {}) {
1459
+ if (!gxPath) return { version: null, major: null, source: 'unavailable' };
1460
+
1461
+ let unresolvedVersionFile = null;
1357
1462
  const candidates = [
1358
1463
  path.join(gxPath, 'version.txt'),
1359
1464
  path.join(gxPath, 'Version.txt'),
@@ -1362,11 +1467,138 @@ function readGeneXusVersionFromInstall(gxPath) {
1362
1467
  for (const candidate of candidates) {
1363
1468
  try {
1364
1469
  const raw = fs.readFileSync(candidate, 'utf8').trim();
1365
- if (raw) return raw.split(/\r?\n/)[0].trim();
1470
+ const version = raw.split(/\r?\n/)[0].trim();
1471
+ if (version) {
1472
+ const major = getGeneXusMajor(version);
1473
+ if (major) return { version, major, source: 'version-file' };
1474
+ unresolvedVersionFile = unresolvedVersionFile || version;
1475
+ }
1366
1476
  } catch {
1367
1477
  }
1368
1478
  }
1369
- return null;
1479
+
1480
+ const readExecutableVersion = typeof options.readExecutableVersion === 'function'
1481
+ ? options.readExecutableVersion
1482
+ : readExecutableProductVersion;
1483
+ const executableVersion = readExecutableVersion(path.join(gxPath, 'GeneXus.exe'));
1484
+ if (executableVersion) {
1485
+ return {
1486
+ version: executableVersion,
1487
+ major: getGeneXusMajor(executableVersion),
1488
+ source: 'executable-metadata'
1489
+ };
1490
+ }
1491
+
1492
+ const pathMatch = path.basename(String(gxPath)).match(/^GeneXus\s*(\d+)/i);
1493
+ if (pathMatch && fs.existsSync(gxPath)) return { version: null, major: pathMatch[1], source: 'path-name' };
1494
+ if (unresolvedVersionFile) return { version: unresolvedVersionFile, major: null, source: 'version-file' };
1495
+ return { version: null, major: null, source: 'unavailable' };
1496
+ }
1497
+
1498
+ function readGeneXusVersionFromInstall(gxPath, options) {
1499
+ return readGeneXusInstallationIdentity(gxPath, options).version;
1500
+ }
1501
+
1502
+ function isWellFormedXml(source) {
1503
+ const xml = String(source || '').replace(/^\uFEFF/, '');
1504
+ const stack = [];
1505
+ let rootCount = 0;
1506
+ let index = 0;
1507
+ while (true) {
1508
+ const open = xml.indexOf('<', index);
1509
+ if (open < 0) break;
1510
+ if (xml.startsWith('<!--', open)) {
1511
+ const endComment = xml.indexOf('-->', open + 4);
1512
+ if (endComment < 0) return false;
1513
+ index = endComment + 3;
1514
+ continue;
1515
+ }
1516
+ if (xml.startsWith('<![CDATA[', open)) {
1517
+ const endCdata = xml.indexOf(']]>', open + 9);
1518
+ if (endCdata < 0) return false;
1519
+ index = endCdata + 3;
1520
+ continue;
1521
+ }
1522
+ if (xml.startsWith('<?', open)) {
1523
+ const endInstruction = xml.indexOf('?>', open + 2);
1524
+ if (endInstruction < 0) return false;
1525
+ index = endInstruction + 2;
1526
+ continue;
1527
+ }
1528
+
1529
+ let end = open + 1;
1530
+ let quote = null;
1531
+ for (; end < xml.length; end++) {
1532
+ const character = xml[end];
1533
+ if (quote) {
1534
+ if (character === quote) quote = null;
1535
+ } else if (character === '"' || character === "'") {
1536
+ quote = character;
1537
+ } else if (character === '>') {
1538
+ break;
1539
+ }
1540
+ }
1541
+ if (end >= xml.length || quote) return false;
1542
+
1543
+ const body = xml.slice(open + 1, end).trim();
1544
+ if (!body || body.startsWith('!')) {
1545
+ index = end + 1;
1546
+ continue;
1547
+ }
1548
+ if (body.startsWith('/')) {
1549
+ const closing = body.slice(1).trim();
1550
+ const closingMatch = closing.match(/^([A-Za-z_][A-Za-z0-9_.:-]*)\s*$/);
1551
+ if (!closingMatch || stack.length === 0 || stack[stack.length - 1] !== closingMatch[1]) return false;
1552
+ stack.pop();
1553
+ } else {
1554
+ const opening = body.replace(/\/\s*$/, '').trim();
1555
+ const openingMatch = opening.match(/^([A-Za-z_][A-Za-z0-9_.:-]*)\b/);
1556
+ if (!openingMatch) return false;
1557
+ if (stack.length === 0) rootCount++;
1558
+ if (!body.endsWith('/')) stack.push(openingMatch[1]);
1559
+ }
1560
+ index = end + 1;
1561
+ }
1562
+ return rootCount === 1 && stack.length === 0;
1563
+ }
1564
+
1565
+ function readGeneXusKbIdentity(kbPath) {
1566
+ if (!kbPath) return { version: null, major: null, source: 'unavailable', reason: 'missing-kb-path' };
1567
+
1568
+ let gxwFiles;
1569
+ try {
1570
+ gxwFiles = fs.readdirSync(kbPath)
1571
+ .filter((fileName) => fileName.toLowerCase().endsWith('.gxw'))
1572
+ .map((fileName) => path.join(kbPath, fileName));
1573
+ } catch {
1574
+ return { version: null, major: null, source: 'unavailable', reason: 'unreadable-kb-path' };
1575
+ }
1576
+ if (gxwFiles.length === 0) return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
1577
+ if (gxwFiles.length > 1) return { version: null, major: null, source: 'unavailable', reason: 'multiple-gxw' };
1578
+
1579
+ try {
1580
+ const source = fs.readFileSync(gxwFiles[0], 'utf8');
1581
+ if (!source.trim()) return { version: null, major: null, source: 'unavailable', reason: 'empty-gxw' };
1582
+ if (!isWellFormedXml(source)) return { version: null, major: null, source: 'unavailable', reason: 'malformed-gxw' };
1583
+ const raw = source.replace(/<!--[\s\S]*?-->/g, '');
1584
+ for (const field of ['VersionNumber', 'FriendlyVersion']) {
1585
+ const pattern = new RegExp(`<(?:(?:[\\w.-]+):)?${field}\\b[^>]*>([\\s\\S]*?)<\\/(?:(?:[\\w.-]+):)?${field}\\s*>`, 'i');
1586
+ const match = raw.match(pattern);
1587
+ const version = match && match[1] ? match[1].replace(/<[^>]+>/g, '').trim() : '';
1588
+ if (version) return { version, major: getGeneXusMajor(version), source: 'gxw-version' };
1589
+ }
1590
+ return { version: null, major: null, source: 'unavailable', reason: 'missing-gxw-version' };
1591
+ } catch {
1592
+ return { version: null, major: null, source: 'unavailable', reason: 'unreadable-gxw' };
1593
+ }
1594
+ }
1595
+
1596
+ function compareGeneXusKbAndInstallation(kbPath, gxPath) {
1597
+ const kb = readGeneXusKbIdentity(kbPath);
1598
+ const gx = readGeneXusInstallationIdentity(gxPath);
1599
+ let status = 'unresolved';
1600
+ if (kb.major && gx.major) status = kb.major === gx.major ? 'match' : 'mismatch';
1601
+ return { status, kb, gx };
1370
1602
  }
1371
1603
 
1372
1604
  function normalizeKbCatalog(raw) {
@@ -1497,21 +1729,42 @@ function applyLauncherConfigOrExit({ cwd, stderr, quiet }) {
1497
1729
  return { ok: true };
1498
1730
  }
1499
1731
 
1500
- const foundGxPath = discoverGeneXusInstallation();
1501
- if (!foundGxPath) {
1502
- log('[genexus-mcp] ERROR: No config.json found and GeneXus installation auto-discovery failed.');
1732
+ if (!directoryLooksLikeKnowledgeBase(cwd)) {
1733
+ log('[genexus-mcp] ERROR: Zero-config failed because current directory is not a GeneXus KB.');
1734
+ log(`[genexus-mcp] CWD: ${cwd}`);
1503
1735
  log('[genexus-mcp] Fix with: npx genexus-mcp init --interactive');
1504
1736
  return { ok: false };
1505
1737
  }
1506
1738
 
1507
- if (!directoryLooksLikeKnowledgeBase(cwd)) {
1508
- log('[genexus-mcp] ERROR: Zero-config failed because current directory is not a GeneXus KB.');
1509
- log(`[genexus-mcp] CWD: ${cwd}`);
1739
+ const kbIdentity = readGeneXusKbIdentity(cwd);
1740
+ if (!kbIdentity.major) {
1741
+ log('[genexus-mcp] ERROR: Zero-config could not determine the KB GeneXus major safely.');
1742
+ log('[genexus-mcp] Fix with: npx genexus-mcp init --kb "<kbPath>" --gx "<geneXusPath>"');
1743
+ return { ok: false };
1744
+ }
1745
+
1746
+ const foundGxPath = discoverGeneXusInstallation(kbIdentity.major);
1747
+ if (!foundGxPath) {
1748
+ log('[genexus-mcp] ERROR: No GeneXus installation matching the KB major was auto-discovered.');
1749
+ log(`[genexus-mcp] KB major: ${kbIdentity.major}`);
1510
1750
  log('[genexus-mcp] Fix with: npx genexus-mcp init --interactive');
1511
1751
  return { ok: false };
1512
1752
  }
1513
1753
 
1514
- log(`[genexus-mcp] Auto-discovered GeneXus at: ${foundGxPath}`);
1754
+ const compatibility = compareGeneXusKbAndInstallation(cwd, foundGxPath);
1755
+ if (compatibility.status === 'mismatch') {
1756
+ log(`[genexus-mcp] ERROR: Auto-discovered SDK major ${compatibility.gx.major} does not match KB major ${compatibility.kb.major}.`);
1757
+ log('[genexus-mcp] Fix with: npx genexus-mcp init --kb "<kbPath>" --gx "<matching GeneXus path>"');
1758
+ return { ok: false };
1759
+ }
1760
+ if (compatibility.status !== 'match') {
1761
+ log('[genexus-mcp] ERROR: Zero-config could not verify that the discovered GeneXus SDK matches the KB major.');
1762
+ log(`[genexus-mcp] KB major: ${compatibility.kb.major}; SDK detection: ${compatibility.gx.source}`);
1763
+ log('[genexus-mcp] Fix with: npx genexus-mcp init --kb "<kbPath>" --gx "<matching GeneXus path>"');
1764
+ return { ok: false };
1765
+ }
1766
+
1767
+ log(`[genexus-mcp] Auto-discovered GeneXus ${kbIdentity.major} at: ${foundGxPath}`);
1515
1768
  log(`[genexus-mcp] Generating default config.json for KB at: ${cwd}`);
1516
1769
 
1517
1770
  const defaultConfig = generateConfig(foundGxPath, cwd);
@@ -1525,6 +1778,8 @@ module.exports = {
1525
1778
  generateConfig,
1526
1779
  getGatewayExePath,
1527
1780
  getToolDefinitionsPath,
1781
+ getGeneXusVersionCatalog,
1782
+ getGeneXusCatalogEntries,
1528
1783
  discoverGeneXusInstallation,
1529
1784
  discoverGeneXusFromRegistry,
1530
1785
  discoverKnowledgeBase,
@@ -1541,7 +1796,11 @@ module.exports = {
1541
1796
  listSupportedClientIds,
1542
1797
  filterClientTargets,
1543
1798
  getLocalAppDataCacheDir,
1799
+ getGeneXusMajor,
1800
+ readGeneXusInstallationIdentity,
1544
1801
  readGeneXusVersionFromInstall,
1802
+ readGeneXusKbIdentity,
1803
+ compareGeneXusKbAndInstallation,
1545
1804
  readKbCatalog,
1546
1805
  addKbToConfig,
1547
1806
  removeKbFromConfig,