genexus-mcp 3.5.3 → 3.6.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/cli/lib/config.js CHANGED
@@ -88,9 +88,12 @@ function getGeneXusVersionCatalog() {
88
88
  const catalog = JSON.parse(fs.readFileSync(candidate, 'utf8'));
89
89
  if (!catalog || !catalog.primaryMajor || !Array.isArray(catalog.supportedMajors)) continue;
90
90
  const entries = catalog.supportedMajors.filter((entry) => entry && /^\d+$/.test(String(entry.major)));
91
+ const legacyEntries = Array.isArray(catalog.legacyMajors)
92
+ ? catalog.legacyMajors.filter((entry) => entry && /^(?:10\.[1-3]|\d+)$/.test(String(entry.major)))
93
+ : [];
91
94
  const primary = entries.find((entry) => String(entry.major) === String(catalog.primaryMajor));
92
95
  if (entries.length > 0 && primary?.defaultInstallPath) {
93
- return { ...catalog, supportedMajors: entries };
96
+ return { ...catalog, supportedMajors: entries, legacyMajors: legacyEntries };
94
97
  }
95
98
  } catch {
96
99
  }
@@ -101,13 +104,24 @@ function getGeneXusVersionCatalog() {
101
104
  { major: '17', displayName: 'GeneXus 17', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXus17Trial' },
102
105
  { major: '18', displayName: 'GeneXus 18', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXus18' }
103
106
  ],
107
+ legacyMajors: [
108
+ { major: '10.3', displayName: 'GeneXus Evolution 3', defaultInstallPath: 'C:\\Program Files (x86)\\GeneXus\\GeneXusXEv3' },
109
+ { major: '9', displayName: 'GeneXus 9.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 9.0' },
110
+ { major: '8', displayName: 'GeneXus 8.0', defaultInstallPath: 'C:\\Program Files (x86)\\ARTech\\GeneXus\\GeneXus 8.0' }
111
+ ],
104
112
  source: 'built-in-fallback'
105
113
  };
106
114
  }
107
115
 
116
+ function hasGeneXusExecutable(gxPath) {
117
+ return Boolean(gxPath) && ['genexus.exe', 'gx.exe']
118
+ .some((name) => fs.existsSync(path.join(gxPath, name)));
119
+ }
120
+
108
121
  function getGeneXusCatalogEntries(preferredMajor = null) {
109
122
  const catalog = getGeneXusVersionCatalog();
110
- return [...catalog.supportedMajors].sort((left, right) => {
123
+ const all = [...catalog.supportedMajors, ...(catalog.legacyMajors || [])];
124
+ return all.sort((left, right) => {
111
125
  const leftPreferred = preferredMajor !== null && String(left.major) === String(preferredMajor);
112
126
  const rightPreferred = preferredMajor !== null && String(right.major) === String(preferredMajor);
113
127
  if (leftPreferred !== rightPreferred) return Number(rightPreferred) - Number(leftPreferred);
@@ -144,7 +158,7 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
144
158
  const match = out.match(/InstallationDirectory\s+REG_SZ\s+(.+?)\r?\n/i);
145
159
  if (match) {
146
160
  const candidate = match[1].trim().replace(/[\\/]+$/, '');
147
- if (candidate && fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
161
+ if (candidate && hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
148
162
  return candidate;
149
163
  }
150
164
  }
@@ -163,7 +177,7 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
163
177
  const match = out.match(/InstallPath\s+REG_SZ\s+(.+?)\r?\n/i);
164
178
  if (match) {
165
179
  const candidate = match[1].trim().replace(/[\\/]+$/, '');
166
- if (candidate && fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
180
+ if (candidate && hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
167
181
  }
168
182
  } catch {
169
183
  }
@@ -178,7 +192,7 @@ function discoverGeneXusFromRegistry(preferredMajor = null) {
178
192
  function discoverGeneXusInstallation(preferredMajor = null) {
179
193
  if (process.env.GENEXUS_HOME) {
180
194
  const candidate = process.env.GENEXUS_HOME.replace(/[\\/]+$/, '');
181
- if (fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
195
+ if (hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) return candidate;
182
196
  }
183
197
 
184
198
  const fromRegistry = discoverGeneXusFromRegistry(preferredMajor);
@@ -196,7 +210,11 @@ function discoverGeneXusInstallation(preferredMajor = null) {
196
210
  const entries = getGeneXusCatalogEntries(preferredMajor);
197
211
  const seen = new Set();
198
212
  for (const base of programDirs) {
199
- const root = path.join(base, 'GeneXus');
213
+ const roots = [
214
+ path.join(base, 'GeneXus'),
215
+ path.join(base, 'Artech', 'GeneXus')
216
+ ];
217
+ for (const root of roots) {
200
218
  const key = root.toLowerCase();
201
219
  if (seen.has(key)) continue;
202
220
  seen.add(key);
@@ -208,7 +226,7 @@ function discoverGeneXusInstallation(preferredMajor = null) {
208
226
  for (const ver of candidateNames) {
209
227
  if (!ver) continue;
210
228
  const candidate = path.join(root, ver);
211
- if (fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
229
+ if (hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
212
230
  return candidate;
213
231
  }
214
232
  }
@@ -221,13 +239,14 @@ function discoverGeneXusInstallation(preferredMajor = null) {
221
239
  const majorMatch = entry.match(/^GeneXus\s*(\d+)/i);
222
240
  if (majorMatch && !entries.some((item) => String(item.major) === majorMatch[1])) continue;
223
241
  const candidate = path.join(root, entry);
224
- if (fs.existsSync(path.join(candidate, 'genexus.exe')) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
242
+ if (hasGeneXusExecutable(candidate) && matchesPreferredGeneXusMajor(candidate, preferredMajor)) {
225
243
  return candidate;
226
244
  }
227
245
  }
228
246
  }
229
247
  } catch {
230
248
  }
249
+ }
231
250
  }
232
251
 
233
252
  const fromPath = discoverGeneXusFromPath(preferredMajor);
@@ -240,15 +259,22 @@ function discoverGeneXusFromPath(preferredMajor = null) {
240
259
  if (process.platform !== 'win32') return null;
241
260
  try {
242
261
  const { execFileSync } = require('child_process');
243
- const out = execFileSync('where.exe', ['genexus.exe'], {
244
- encoding: 'utf8',
245
- stdio: ['ignore', 'pipe', 'ignore'],
246
- windowsHide: true,
247
- timeout: 3000
248
- });
249
- const first = out.split(/\r?\n/).map((s) => s.trim()).find(Boolean);
250
- if (first && fs.existsSync(first) && matchesPreferredGeneXusMajor(path.dirname(first), preferredMajor)) {
251
- return path.dirname(first);
262
+ for (const executable of ['genexus.exe', 'gx.exe']) {
263
+ let out;
264
+ try {
265
+ out = execFileSync('where.exe', [executable], {
266
+ encoding: 'utf8',
267
+ stdio: ['ignore', 'pipe', 'ignore'],
268
+ windowsHide: true,
269
+ timeout: 3000
270
+ });
271
+ } catch {
272
+ continue;
273
+ }
274
+ const first = out.split('\n').map((s) => s.trim()).find(Boolean);
275
+ if (first && fs.existsSync(first) && matchesPreferredGeneXusMajor(path.dirname(first), preferredMajor)) {
276
+ return path.dirname(first);
277
+ }
252
278
  }
253
279
  } catch {
254
280
  }
@@ -708,6 +734,12 @@ function resolveOpenCodeConfigPath(xdgConfig) {
708
734
  return path.join(xdgConfig, 'opencode', 'opencode.json');
709
735
  }
710
736
 
737
+ function alternateOpenCodeConfigPath(filePath) {
738
+ return path.extname(filePath).toLowerCase() === '.jsonc'
739
+ ? filePath.slice(0, -1)
740
+ : filePath.replace(/\.json$/i, '.jsonc');
741
+ }
742
+
711
743
  // VS Code stores its user profile (and native MCP mcp.json) in a per-platform
712
744
  // location. `variant` is 'Code' (stable) or 'Code - Insiders'.
713
745
  function vscodeUserDir(variant, { appData, macAppSupport, xdgConfig }) {
@@ -807,6 +839,7 @@ function getClientConfigTargets() {
807
839
  name: 'OpenCode (CLI)',
808
840
  format: 'opencode',
809
841
  path: resolveOpenCodeConfigPath(xdgConfig),
842
+ alternatePaths: [alternateOpenCodeConfigPath(resolveOpenCodeConfigPath(xdgConfig))],
810
843
  installMarkers: [
811
844
  path.join(xdgConfig, 'opencode'),
812
845
  path.join(home, '.local', 'share', 'opencode')
@@ -826,6 +859,7 @@ function getClientConfigTargets() {
826
859
  name: 'OpenCode Desktop',
827
860
  format: 'opencode',
828
861
  path: resolveOpenCodeConfigPath(xdgConfig),
862
+ alternatePaths: [alternateOpenCodeConfigPath(resolveOpenCodeConfigPath(xdgConfig))],
829
863
  detectByMarkerOnly: true,
830
864
  installMarkers: [
831
865
  path.join(localAppData, 'Programs', '@opencode-aidesktop'),
@@ -937,21 +971,27 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
937
971
  reason: null,
938
972
  structuralState: 'indeterminate',
939
973
  semanticState: 'unknown',
940
- semanticReason: null
974
+ semanticReason: null,
975
+ pathDrift: false,
976
+ pathDriftReason: null
941
977
  };
942
978
  const invalid = (reason, structuralState = 'present') => ({
943
979
  stale: true,
944
980
  reason,
945
981
  structuralState,
946
982
  semanticState: 'invalid',
947
- semanticReason: reason
983
+ semanticReason: reason,
984
+ pathDrift: false,
985
+ pathDriftReason: null
948
986
  });
949
987
  const valid = (structuralState = 'present') => ({
950
988
  stale: false,
951
989
  reason: null,
952
990
  structuralState,
953
991
  semanticState: 'valid',
954
- semanticReason: null
992
+ semanticReason: null,
993
+ pathDrift: false,
994
+ pathDriftReason: null
955
995
  });
956
996
  const missingLauncherReason = 'configured launcher does not exist on disk';
957
997
 
@@ -961,7 +1001,9 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
961
1001
  reason: null,
962
1002
  structuralState: 'not-registered',
963
1003
  semanticState: 'not-applicable',
964
- semanticReason: null
1004
+ semanticReason: null,
1005
+ pathDrift: false,
1006
+ pathDriftReason: null
965
1007
  };
966
1008
  }
967
1009
  if (typeof entry !== 'object') {
@@ -973,7 +1015,9 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
973
1015
  reason: null,
974
1016
  structuralState: 'not-applicable',
975
1017
  semanticState: 'not-applicable',
976
- semanticReason: null
1018
+ semanticReason: null,
1019
+ pathDrift: false,
1020
+ pathDriftReason: null
977
1021
  };
978
1022
  }
979
1023
 
@@ -1004,13 +1048,21 @@ function clientCommandHealth(entry, client = null, { fs: fileSystem = fs } = {})
1004
1048
  };
1005
1049
  }
1006
1050
  if (!exists) return invalid(missingLauncherReason, 'missing');
1051
+ // A gateway exe that exists at a different absolute path is a *different,
1052
+ // working* install — a local checkout's publish/ copy, a fixed-path install,
1053
+ // or another package cache — not a broken registration. Reporting it as
1054
+ // `stale` made `npx genexus-mcp clients` tell a checkout operator to
1055
+ // re-register, which rewrote the harness away from the checkout gateway
1056
+ // (Issue #210). The drift is still surfaced (as `pathDrift`, never `stale`)
1057
+ // and doctor's `client_config_sync` check keeps the "not this CLI's packaged
1058
+ // gateway" warning with its own remediation.
1007
1059
  if (client && client.preferDirectGateway) {
1008
1060
  const currentPackageGateway = getGatewayExePath();
1009
- if (fs.existsSync(currentPackageGateway) && normalizeExePath(command) !== normalizeExePath(currentPackageGateway)) {
1061
+ if (fileSystem.existsSync(currentPackageGateway) && normalizeExePath(command) !== normalizeExePath(currentPackageGateway)) {
1010
1062
  return {
1011
1063
  ...valid('present'),
1012
- stale: true,
1013
- reason: 'configured launcher points at a different package gateway; re-register the client'
1064
+ pathDrift: true,
1065
+ pathDriftReason: `configured gateway differs from this CLI's gateway (${currentPackageGateway})`
1014
1066
  };
1015
1067
  }
1016
1068
  }
@@ -1131,13 +1183,16 @@ function clientsStatus(opts = {}) {
1131
1183
  serverName,
1132
1184
  isThirdParty: Boolean(isThirdParty),
1133
1185
  writeSupported: client.writeSupported !== false,
1134
- configPath: client.path,
1186
+ configPath: entry && entry.configPath ? entry.configPath : client.path,
1187
+ configPaths: entry && entry.configPath ? [entry.configPath] : [client.path],
1135
1188
  command: entry && entry.command ? entry.command : null,
1136
1189
  args: entry && Array.isArray(entry.args) ? entry.args : [],
1137
1190
  url: entry && entry.url ? entry.url : null,
1138
1191
  launcherStructuralState: health.structuralState,
1139
1192
  launcherSemanticState: health.semanticState,
1140
1193
  launcherSemanticReason: health.semanticReason,
1194
+ launcherPathDrift: health.pathDrift,
1195
+ launcherPathDriftReason: health.pathDriftReason,
1141
1196
  commandStale: health.stale,
1142
1197
  commandStaleReason: health.reason || (isThirdParty ? 'configured as third-party / HTTP MCP server (e.g. official GeneXus MCP)' : null),
1143
1198
  detectedAt: det.markerHit || (det.hasConfig ? client.path : null),
@@ -1426,7 +1481,7 @@ function getOpenCodeMcpContainer(cfgObj) {
1426
1481
  };
1427
1482
  }
1428
1483
 
1429
- function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false, fs: fileSystem = fs } = {}) {
1484
+ function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName = DEFAULT_MCP_SERVER_NAME, force = false, globalConfig = false, alternatePaths = [], fs: fileSystem = fs } = {}) {
1430
1485
  const parsed = fileSystem.existsSync(filePath) ? readJsonFileSafe(filePath, fileSystem) : {};
1431
1486
  if (parsed === null) throw new Error('Invalid JSON');
1432
1487
  const cfgObj = parsed || {};
@@ -1470,10 +1525,20 @@ function applyOpenCodeJson(filePath, launcher, targetConfigPath, { serverName =
1470
1525
  }
1471
1526
  }
1472
1527
  writeClientJson(filePath, cfgObj, fileSystem);
1528
+ for (const alternatePath of alternatePaths || []) {
1529
+ if (!fileSystem.existsSync(alternatePath)) continue;
1530
+ applyOpenCodeJson(alternatePath, launcher, targetConfigPath, {
1531
+ serverName,
1532
+ force,
1533
+ globalConfig,
1534
+ alternatePaths: [],
1535
+ fs: fileSystem
1536
+ });
1537
+ }
1473
1538
  }
1474
1539
 
1475
- function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, fs: fileSystem = fs } = {}) {
1476
- const parsed = readJsonFileSafe(filePath);
1540
+ function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, alternatePaths = [], fs: fileSystem = fs } = {}) {
1541
+ const parsed = readJsonFileSafe(filePath, fileSystem);
1477
1542
  if (parsed === null) throw new Error('Invalid JSON');
1478
1543
  const cfgObj = parsed || {};
1479
1544
  if (!cfgObj.mcp || typeof cfgObj.mcp !== 'object') return false;
@@ -1503,9 +1568,17 @@ function removeOpenCodeJson(filePath, { serverName = DEFAULT_MCP_SERVER_NAME, fs
1503
1568
  }
1504
1569
  }
1505
1570
  }
1506
- if (!removedAny) return false;
1507
- writeClientJson(filePath, cfgObj, fileSystem);
1508
- return true;
1571
+ if (removedAny) writeClientJson(filePath, cfgObj, fileSystem);
1572
+ let removedAlternate = false;
1573
+ for (const alternatePath of alternatePaths || []) {
1574
+ if (!fileSystem.existsSync(alternatePath)) continue;
1575
+ removedAlternate = removeOpenCodeJson(alternatePath, {
1576
+ serverName,
1577
+ alternatePaths: [],
1578
+ fs: fileSystem
1579
+ }) || removedAlternate;
1580
+ }
1581
+ return removedAny || removedAlternate;
1509
1582
  }
1510
1583
 
1511
1584
  function extractCodexTomlEntry(content, serverName = DEFAULT_MCP_SERVER_NAME) {
@@ -1650,8 +1723,49 @@ function normalizeExePath(p) {
1650
1723
  return s;
1651
1724
  }
1652
1725
 
1726
+ function readOpenCodeCommandEntry(filePath, serverName, fileSystem) {
1727
+ if (!fileSystem.existsSync(filePath)) return null;
1728
+ const parsed = readJsonFileSafe(filePath, fileSystem);
1729
+ if (!parsed || typeof parsed !== 'object') return null;
1730
+ let entry = parsed.mcp?.servers?.[serverName] || parsed.mcp?.[serverName];
1731
+ if (!entry && serverName === DEFAULT_MCP_SERVER_NAME) {
1732
+ const legacyGx = parsed.mcp?.servers?.genexus || parsed.mcp?.genexus;
1733
+ if (legacyGx && !isThirdPartyMcpEntry(legacyGx)) entry = legacyGx;
1734
+ else entry = parsed.mcp?.servers?.genexus18 || parsed.mcp?.genexus18;
1735
+ }
1736
+ if (!entry) return null;
1737
+ if (Array.isArray(entry.command) && entry.command.length > 0) {
1738
+ return {
1739
+ command: entry.command[0],
1740
+ args: entry.command.slice(1),
1741
+ url: entry.url || null,
1742
+ type: entry.type || null,
1743
+ environment: entry.environment || null,
1744
+ raw: entry,
1745
+ configPath: filePath
1746
+ };
1747
+ }
1748
+ return {
1749
+ command: null,
1750
+ args: [],
1751
+ url: entry.url || null,
1752
+ type: entry.type || null,
1753
+ environment: entry.environment || null,
1754
+ raw: entry,
1755
+ configPath: filePath
1756
+ };
1757
+ }
1758
+
1653
1759
  function readClientCommandEntry(client, serverName = DEFAULT_MCP_SERVER_NAME, { fs: fileSystem = fs } = {}) {
1654
1760
  if (client.writeSupported === false) return null;
1761
+ if (client.format === 'opencode') {
1762
+ const paths = [client.path, ...(client.alternatePaths || [])];
1763
+ for (const configPath of paths) {
1764
+ const entry = readOpenCodeCommandEntry(configPath, serverName, fileSystem);
1765
+ if (entry) return entry;
1766
+ }
1767
+ return null;
1768
+ }
1655
1769
  if (!fileSystem.existsSync(client.path)) return null;
1656
1770
  try {
1657
1771
  if (client.format === 'mcpServers') {
@@ -1752,7 +1866,7 @@ function getLocalAppDataCacheDir() {
1752
1866
  }
1753
1867
 
1754
1868
  function getGeneXusMajor(version) {
1755
- const match = String(version || '').match(/^\s*(\d+)/);
1869
+ const match = String(version || '').match(/^\s*(10\.[1-3](?!\d)|\d+)/);
1756
1870
  return match ? match[1] : null;
1757
1871
  }
1758
1872
 
@@ -1809,7 +1923,10 @@ function readGeneXusInstallationIdentity(gxPath, options = {}) {
1809
1923
  const readExecutableVersion = typeof options.readExecutableVersion === 'function'
1810
1924
  ? options.readExecutableVersion
1811
1925
  : readExecutableProductVersion;
1812
- const executableVersion = readExecutableVersion(path.join(gxPath, 'GeneXus.exe'));
1926
+ let executableVersion = readExecutableVersion(path.join(gxPath, 'GeneXus.exe'));
1927
+ if (!executableVersion) {
1928
+ executableVersion = readExecutableVersion(path.join(gxPath, 'gx.exe'));
1929
+ }
1813
1930
  if (executableVersion) {
1814
1931
  return {
1815
1932
  version: executableVersion,
@@ -1895,14 +2012,24 @@ function readGeneXusKbIdentity(kbPath) {
1895
2012
  if (!kbPath) return { version: null, major: null, source: 'unavailable', reason: 'missing-kb-path' };
1896
2013
 
1897
2014
  let gxwFiles;
2015
+ let gxiFiles = [];
1898
2016
  try {
1899
- gxwFiles = fs.readdirSync(kbPath)
2017
+ const allFiles = fs.readdirSync(kbPath);
2018
+ gxwFiles = allFiles
1900
2019
  .filter((fileName) => fileName.toLowerCase().endsWith('.gxw'))
1901
2020
  .map((fileName) => path.join(kbPath, fileName));
2021
+ gxiFiles = allFiles
2022
+ .filter((fileName) => fileName.toLowerCase().endsWith('.gxi'))
2023
+ .map((fileName) => path.join(kbPath, fileName));
1902
2024
  } catch {
1903
2025
  return { version: null, major: null, source: 'unavailable', reason: 'unreadable-kb-path' };
1904
2026
  }
1905
- if (gxwFiles.length === 0) return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
2027
+ if (gxwFiles.length === 0) {
2028
+ if (gxiFiles.length > 0) {
2029
+ return { version: '9.0', major: '9', source: 'gxi-classic', reason: null };
2030
+ }
2031
+ return { version: null, major: null, source: 'unavailable', reason: 'no-gxw' };
2032
+ }
1906
2033
  if (gxwFiles.length > 1) return { version: null, major: null, source: 'unavailable', reason: 'multiple-gxw' };
1907
2034
 
1908
2035
  try {