genexus-mcp 1.3.0 → 1.3.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.
@@ -871,6 +871,10 @@ function commandHelpMap() {
871
871
  usage: 'genexus-mcp llm help [--full] [--fields f1,f2] [--format toon|json|text]',
872
872
  examples: ['genexus-mcp llm help --format json', 'genexus-mcp llm help --full --format json']
873
873
  },
874
+ update: {
875
+ usage: 'genexus-mcp update [--format toon|json|text]',
876
+ examples: ['genexus-mcp update', 'genexus-mcp update --format json']
877
+ },
874
878
  layout: {
875
879
  usage: 'genexus-mcp layout status [--title "GeneXus"] [--format ...] OR genexus-mcp layout run --action <focus|activate-layout|activate-tab|send-keys|type-text|click> [--tab "Layout"] [--keys "..."] [--text "..."] [--x N --y N] [--title "..."] [--format ...] OR genexus-mcp layout inspect [--tab "Layout"] [--limit N] [--full] [--title "..."] [--format ...]',
876
880
  examples: ['genexus-mcp layout status --format json', 'genexus-mcp layout run --action activate-tab --tab "Layout" --format json', 'genexus-mcp layout inspect --tab Layout --format json']
@@ -935,7 +939,7 @@ async function handleHelp(targetCommand, ctx) {
935
939
  bin: binPath,
936
940
  command: 'genexus-mcp',
937
941
  description: 'GeneXus MCP launcher and AXI-oriented utility CLI',
938
- commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'layout status', 'layout run', 'layout inspect', 'init', 'llm help', 'help'],
942
+ commands: ['home', 'axi home', 'status', 'doctor', 'tools list', 'config show', 'layout status', 'layout run', 'layout inspect', 'init', 'llm help', 'update', 'help'],
939
943
  defaults: { format: 'toon', limit: 100 }
940
944
  },
941
945
  help: [
package/cli/index.js CHANGED
@@ -22,6 +22,7 @@ const {
22
22
  usageEnvelope,
23
23
  commandHelpMap
24
24
  } = require('./commands/axi');
25
+ const { startBackgroundUpdateCheck, handleUpdate } = require('./lib/update-check');
25
26
 
26
27
  const EXIT_CODES = {
27
28
  OK: 0,
@@ -43,7 +44,7 @@ const GLOBAL_DEFAULTS = {
43
44
  help: false
44
45
  };
45
46
 
46
- const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'help', 'home', 'axi', 'llm', 'layout']);
47
+ const KNOWN_COMMANDS = new Set(['status', 'doctor', 'tools', 'config', 'init', 'setup', 'help', 'home', 'axi', 'llm', 'layout', 'update']);
47
48
 
48
49
  function parseArgs(argv) {
49
50
  const result = {
@@ -332,12 +333,17 @@ function resolveMetaCommand(parsed, targetHelp) {
332
333
  if (parsed.subcommand === 'inspect') return 'layout.inspect';
333
334
  return 'layout.status';
334
335
  }
336
+ if (parsed.command === 'update') return 'update';
335
337
  return parsed.command || 'unknown';
336
338
  }
337
339
 
338
340
  async function main(argv) {
339
341
  const parsed = parseArgs(argv);
340
342
 
343
+ if (parsed.command !== 'update') {
344
+ startBackgroundUpdateCheck({ quiet: parsed.options.quiet });
345
+ }
346
+
341
347
  if (!parsed.command) {
342
348
  return launchGateway(argv, parsed.options);
343
349
  }
@@ -438,6 +444,9 @@ async function main(argv) {
438
444
  case 'init':
439
445
  result = await handleInit(parsed.options, ctx);
440
446
  break;
447
+ case 'update':
448
+ result = await handleUpdate(parsed.options, ctx);
449
+ break;
441
450
  default:
442
451
  writeStructured(
443
452
  process.stdout,
@@ -0,0 +1,214 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const https = require('https');
5
+
6
+ const REPO = 'lennix1337/Genexus18MCP';
7
+ const NPM_PACKAGE = 'genexus-mcp';
8
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
9
+ const FETCH_TIMEOUT_MS = 2000;
10
+
11
+ function getPackageVersion() {
12
+ try {
13
+ const pkg = require('../../package.json');
14
+ return typeof pkg.version === 'string' ? pkg.version : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+
20
+ function getCacheFile() {
21
+ return path.join(os.homedir(), '.genexus-mcp', 'update-check.json');
22
+ }
23
+
24
+ function readCache() {
25
+ try {
26
+ const raw = fs.readFileSync(getCacheFile(), 'utf8');
27
+ const data = JSON.parse(raw);
28
+ if (data && typeof data === 'object') return data;
29
+ } catch {
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function writeCache(data) {
35
+ try {
36
+ const file = getCacheFile();
37
+ fs.mkdirSync(path.dirname(file), { recursive: true });
38
+ fs.writeFileSync(file, JSON.stringify(data), 'utf8');
39
+ } catch {
40
+ }
41
+ }
42
+
43
+ function stripV(v) {
44
+ return typeof v === 'string' ? v.replace(/^v/i, '').trim() : '';
45
+ }
46
+
47
+ function parseSemver(v) {
48
+ const s = stripV(v);
49
+ const m = /^(\d+)\.(\d+)\.(\d+)/.exec(s);
50
+ if (!m) return null;
51
+ return [Number(m[1]), Number(m[2]), Number(m[3])];
52
+ }
53
+
54
+ function compareSemver(a, b) {
55
+ const pa = parseSemver(a);
56
+ const pb = parseSemver(b);
57
+ if (!pa || !pb) return 0;
58
+ for (let i = 0; i < 3; i += 1) {
59
+ if (pa[i] > pb[i]) return 1;
60
+ if (pa[i] < pb[i]) return -1;
61
+ }
62
+ return 0;
63
+ }
64
+
65
+ function fetchLatestRelease() {
66
+ return new Promise((resolve) => {
67
+ const options = {
68
+ hostname: 'api.github.com',
69
+ path: `/repos/${REPO}/releases/latest`,
70
+ method: 'GET',
71
+ headers: {
72
+ 'User-Agent': `${NPM_PACKAGE}-cli`,
73
+ 'Accept': 'application/vnd.github+json'
74
+ }
75
+ };
76
+
77
+ const req = https.request(options, (res) => {
78
+ if (res.statusCode !== 200) {
79
+ res.resume();
80
+ resolve(null);
81
+ return;
82
+ }
83
+ let body = '';
84
+ res.setEncoding('utf8');
85
+ res.on('data', (chunk) => { body += chunk; });
86
+ res.on('end', () => {
87
+ try {
88
+ const json = JSON.parse(body);
89
+ const tag = stripV(json.tag_name || '');
90
+ const url = typeof json.html_url === 'string' ? json.html_url : null;
91
+ if (!tag) { resolve(null); return; }
92
+ resolve({ latestVersion: tag, releaseUrl: url });
93
+ } catch {
94
+ resolve(null);
95
+ }
96
+ });
97
+ });
98
+
99
+ req.on('error', () => resolve(null));
100
+ req.setTimeout(FETCH_TIMEOUT_MS, () => {
101
+ req.destroy();
102
+ resolve(null);
103
+ });
104
+
105
+ req.end();
106
+ if (typeof req.unref === 'function') req.unref();
107
+ });
108
+ }
109
+
110
+ function formatBanner(current, latest, releaseUrl) {
111
+ const lines = [
112
+ `[genexus-mcp] update available: v${current} -> v${latest}`,
113
+ `[genexus-mcp] run: npm install -g ${NPM_PACKAGE}@latest`
114
+ ];
115
+ if (releaseUrl) lines.push(`[genexus-mcp] release: ${releaseUrl}`);
116
+ return lines.join('\n') + '\n';
117
+ }
118
+
119
+ function isDisabled(opts) {
120
+ if (process.env.GENEXUS_MCP_NO_UPDATE_CHECK === '1') return true;
121
+ if (opts && opts.quiet) return true;
122
+ if (!process.stderr || !process.stderr.isTTY) return true;
123
+ return false;
124
+ }
125
+
126
+ function maybePrintCachedBanner(opts) {
127
+ const current = getPackageVersion();
128
+ if (!current) return;
129
+ const cache = readCache();
130
+ if (!cache || !cache.latestVersion) return;
131
+ if (compareSemver(cache.latestVersion, current) > 0) {
132
+ try {
133
+ process.stderr.write(formatBanner(current, cache.latestVersion, cache.releaseUrl || null));
134
+ } catch {
135
+ }
136
+ }
137
+ }
138
+
139
+ function scheduleBackgroundFetch() {
140
+ const cache = readCache();
141
+ const now = Date.now();
142
+ if (cache && typeof cache.checkedAt === 'number' && (now - cache.checkedAt) < CACHE_TTL_MS) {
143
+ return;
144
+ }
145
+
146
+ fetchLatestRelease().then((result) => {
147
+ if (!result) return;
148
+ writeCache({
149
+ checkedAt: Date.now(),
150
+ latestVersion: result.latestVersion,
151
+ releaseUrl: result.releaseUrl
152
+ });
153
+ }).catch(() => {});
154
+ }
155
+
156
+ function startBackgroundUpdateCheck(opts) {
157
+ if (isDisabled(opts)) return;
158
+ maybePrintCachedBanner(opts);
159
+ scheduleBackgroundFetch();
160
+ }
161
+
162
+ async function handleUpdate(_options, ctx) {
163
+ const current = getPackageVersion();
164
+ const result = await fetchLatestRelease();
165
+
166
+ if (!result) {
167
+ return {
168
+ exitCode: ctx.EXIT_CODES.OK,
169
+ envelope: {
170
+ ok: {
171
+ current,
172
+ latest: null,
173
+ updateAvailable: false,
174
+ fetched: false
175
+ },
176
+ help: ['Could not reach GitHub releases API. Check connectivity or retry later.']
177
+ }
178
+ };
179
+ }
180
+
181
+ writeCache({
182
+ checkedAt: Date.now(),
183
+ latestVersion: result.latestVersion,
184
+ releaseUrl: result.releaseUrl
185
+ });
186
+
187
+ const updateAvailable = compareSemver(result.latestVersion, current || '0.0.0') > 0;
188
+ const help = updateAvailable
189
+ ? [`Run: npm install -g ${NPM_PACKAGE}@latest`, result.releaseUrl ? `Release: ${result.releaseUrl}` : null].filter(Boolean)
190
+ : ['Already on latest version.'];
191
+
192
+ return {
193
+ exitCode: ctx.EXIT_CODES.OK,
194
+ envelope: {
195
+ ok: {
196
+ current,
197
+ latest: result.latestVersion,
198
+ releaseUrl: result.releaseUrl,
199
+ updateAvailable,
200
+ installCommand: `npm install -g ${NPM_PACKAGE}@latest`,
201
+ fetched: true
202
+ },
203
+ help
204
+ }
205
+ };
206
+ }
207
+
208
+ module.exports = {
209
+ startBackgroundUpdateCheck,
210
+ handleUpdate,
211
+ compareSemver,
212
+ parseSemver,
213
+ getPackageVersion
214
+ };
package/cli/run.test.js CHANGED
@@ -5,6 +5,7 @@ const path = require('node:path');
5
5
  const os = require('node:os');
6
6
  const fs = require('node:fs');
7
7
  const { renderOutput } = require('./lib/output');
8
+ const { compareSemver } = require('./lib/update-check');
8
9
 
9
10
  const cliPath = path.join(__dirname, 'run.js');
10
11
 
@@ -304,6 +305,24 @@ test('quiet flag suppresses launcher stderr noise', () => {
304
305
  assert.equal(result.stderr.trim(), '');
305
306
  });
306
307
 
308
+ test('update --help returns usage entry', () => {
309
+ const result = runCli(['update', '--help', '--format', 'json']);
310
+ assert.equal(result.status, 0);
311
+
312
+ const parsed = JSON.parse(result.stdout);
313
+ assert.equal(parsed.meta.command, 'help');
314
+ assert.equal(parsed.ok.command, 'update');
315
+ assert.ok(parsed.ok.usage.includes('genexus-mcp update'));
316
+ });
317
+
318
+ test('compareSemver detects newer, older, equal versions', () => {
319
+ assert.equal(compareSemver('1.3.1', '1.3.0'), 1);
320
+ assert.equal(compareSemver('v1.4.0', '1.3.9'), 1);
321
+ assert.equal(compareSemver('1.3.0', '1.3.0'), 0);
322
+ assert.equal(compareSemver('1.2.9', '1.3.0'), -1);
323
+ assert.equal(compareSemver('garbage', '1.0.0'), 0);
324
+ });
325
+
307
326
  test('gateway passthrough remains intact when no AXI subcommand is used', () => {
308
327
  const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'genexus-mcp-test-'));
309
328
  const fakeGateway = path.join(tempRoot, 'fake-gateway.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genexus-mcp",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A high-performance Model Context Protocol (MCP) server for GeneXus 18",
5
5
  "scripts": {
6
6
  "test": "node --test cli/run.test.js",