sap-ai-dev-toolkit 0.9.94 → 0.9.97

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/README.md CHANGED
@@ -338,7 +338,7 @@ Some current npm versions also require install hooks to be approved. If npm repo
338
338
 
339
339
  The setup report uses icons and terminal colors; set `NO_COLOR=1` to disable ANSI colors. Long-running steps (BAS destination discovery, ADT probes, VSP runtime provisioning, `--doctor` checks) show an animated progress line so the terminal never looks stuck; it appears only on interactive terminals and is disabled automatically on CI — set `SAP_AI_DEV_TOOLKIT_DISABLE_SCAN_ANIMATION=true` to turn it off. The table is a weather report, not a bouncer: green **PASS** means the ADT probe responded, red **FAIL** means it failed, and yellow **SKIPPED** means it was skipped. Probe failures do not block MCP registration or startup for destinations you select.
340
340
 
341
- Run `sap-ai-dev --setup` later to change the destination selection or remove generated destination entries. Run `sap-ai-dev --setup --tools` to also choose optional companion tools. Generated destination entries use the global `sap-ai-dev` command when it is on `PATH`; otherwise (for example after a pure npx setup) they automatically launch the pinned package version through `npx`, routed through `cmd /c npx` on Windows so every MCP host can spawn them. Pass `--npx` to force the npx launcher even when a global command exists; companion tools already use `npx` with their own npm packages.
341
+ Run `sap-ai-dev --setup` later to change the destination selection or remove generated destination entries. Run `sap-ai-dev --setup --tools` to also choose optional companion tools. Generated destination entries use the global `sap-ai-dev` command when it is on `PATH` and is the same package version as the setup that writes them; otherwise (for example after a pure npx setup, or when an older global install is still on `PATH`) they automatically launch the pinned package version through `npx`, routed through `cmd /c npx` on Windows so every MCP host can spawn them. Setup warns when it skips a global install of another version. Pass `--npx` to force the npx launcher even when a global command exists; companion tools already use `npx` with their own npm packages.
342
342
 
343
343
  If a setup step is skipped or fails, rerun it from an interactive terminal:
344
344
 
@@ -824,6 +824,7 @@ The response contains a `tools` array. A `RunQuery` entry resembles this excerpt
824
824
  | `SAP_AI_DEV_TOOLKIT_BINARY_URL` | Alternate VSP binary download URL (checksum-verified). Legacy `BAS_VSP_BINARY_URL` still works. |
825
825
  | `SAP_AI_DEV_TOOLKIT_RELEASE_BASE_URL` | Override the base URL that VSP release binaries are downloaded from. |
826
826
  | `SAP_AI_DEV_TOOLKIT_CACHE_DIR` | Binary cache directory. Legacy `BAS_VSP_CACHE_DIR` still works. |
827
+ | `SAP_AI_DEV_TOOLKIT_VERSION` | Written by setup into each destination entry: the toolkit version that configured it. An older `sap-ai-dev` started from that entry hands off to this version through `npx` (see Self-healing modes). |
827
828
  | `BAS_CF_SPACE_GUID`, `BAS_CF_DESTINATION_INSTANCE_GUID`, `BAS_CF_DESTINATION_INSTANCE`, `BAS_CF_DESTINATION_KEY`, `BAS_CF_DESTINATION_NAME`, `BAS_CF_CONNECTIVITY_INSTANCE_GUID`, `BAS_CF_CONNECTIVITY_INSTANCE`, `BAS_CF_CONNECTIVITY_KEY` | Written into generated Cloud Foundry destination entries; they reference service instances and key **names** (never credentials). |
828
829
  | `GO_BINARY` | Explicit Go executable for the repository-only `build:vsp` script; not used during installation. |
829
830
  | `SAP_AI_DEV_TOOLKIT_SKIP_PROBE=true` | Skip destination probes; useful for controlled diagnostics or fixtures. |
@@ -834,6 +835,7 @@ The response contains a `tools` array. A `RunQuery` entry resembles this excerpt
834
835
 
835
836
  VSP children connect to each configured destination URL directly. BAS destination authentication remains selected through VSP's `--proxy-auth` path; Cloud Foundry on-premise destinations continue through the separate Connectivity proxy.
836
837
  Child crash recovery restarts a failed VSP child, re-initializes it, re-registers tools, and retries the interrupted `tools/call` once before surfacing an error. State-changing tools are not retried.
838
+ Stale launcher recovery: when an MCP entry starts a `sap-ai-dev` older than the `SAP_AI_DEV_TOOLKIT_VERSION` setup recorded in it (for example an outdated global install left on `PATH`), the launcher logs a warning and hands off to that version through `npx` on the same stdio, so the host never runs the stale launcher or VSP binary. A source checkout (`npm link`) always runs as-is.
837
839
  Existing installs that still set the previous `BAS_VSP_*` environment variables remain supported. The setup wizard writes new MCP entries with the `SAP_AI_DEV_TOOLKIT_*` names.
838
840
 
839
841
  #### Server logging
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.9.94",
3
+ "version": "0.9.97",
4
4
  "description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, HANA Cloud, Fiori, UI5, and MCP",
5
5
  "author": "Gurkan Yilmaz",
6
6
  "type": "module",
package/src/launcher.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
3
4
  import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
4
5
  import { isIP } from 'node:net';
5
6
  import { dirname, join } from 'node:path';
@@ -8,7 +9,7 @@ import { discoverDestinations, remediation, slugifyDestination, statusRows } fro
8
9
  import { adtBaseUrl, adtPort, detectAdtUrl, discoverSapGuiSystems, hasExplicitPort, hasPathPrefix } from './local-sap-gui.mjs';
9
10
  import { binaryTarget, cacheDirectory, downloadBinary, findBinary } from './binary.mjs';
10
11
  import { MCPProxy } from './mcp-proxy.mjs';
11
- import { installMcpConfig, npxMcpLauncher, repairManagedMcpConfig } from './mcp-config.mjs';
12
+ import { configuredVersionHandoff, installMcpConfig, npxPackageLauncher, repairManagedMcpConfig } from './mcp-config.mjs';
12
13
  import { runSetup } from './setup.mjs';
13
14
  import { resolveConfiguredCloudFoundryDestination } from './cf-destination.mjs';
14
15
  import { createTlsServerNameAdtProxy } from './tls-adt-proxy.mjs';
@@ -268,6 +269,33 @@ function noProxyEnv(env, host) {
268
269
  return { NO_PROXY: noProxy, no_proxy: noProxy };
269
270
  }
270
271
 
272
+ // Runs a child on this process's stdio and mirrors its exit; rejects only when it cannot start.
273
+ async function runOnOwnStdio(command, commandArgs, env) {
274
+ const child = spawn(command, commandArgs, { env, stdio: 'inherit' });
275
+ process.once('SIGINT', () => child.kill('SIGINT'));
276
+ process.once('SIGTERM', () => child.kill('SIGTERM'));
277
+ const [code, signal] = await new Promise((resolve, reject) => {
278
+ child.once('error', reject);
279
+ child.once('exit', (exitCode, exitSignal) => resolve([exitCode, exitSignal]));
280
+ });
281
+ if (signal) process.kill(process.pid, signal);
282
+ process.exitCode = code ?? 1;
283
+ }
284
+
285
+ // A source checkout runs as-is: its developer chose that code.
286
+ async function handOffToConfiguredVersion() {
287
+ const handoff = existsSync(join(root, '.git')) ? null : configuredVersionHandoff(runtimeEnv, pkg.version);
288
+ if (!handoff) return false;
289
+ logLine(`[sap-ai-dev] sap-ai-dev-toolkit ${pkg.version} is older than ${handoff.version}, which configured this MCP entry; self-healing: handing off to sap-ai-dev-toolkit@${handoff.version} through npx. Run "npm install -g sap-ai-dev-toolkit@${handoff.version}" to update the global install.`);
290
+ try {
291
+ await runOnOwnStdio(handoff.command, handoff.args, { ...runtimeEnv, SAP_AI_DEV_TOOLKIT_HANDOFF: pkg.version });
292
+ return true;
293
+ } catch (error) {
294
+ logLine(`[sap-ai-dev] handoff to sap-ai-dev-toolkit@${handoff.version} could not start (${error.message}); continuing with ${pkg.version}`);
295
+ return false;
296
+ }
297
+ }
298
+
271
299
  async function discoverForCommand() {
272
300
  const env = { ...runtimeEnv };
273
301
  if (env.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry') env.SAP_AI_DEV_TOOLKIT_DESTINATION = '';
@@ -349,6 +377,7 @@ async function main() {
349
377
  process.stdout.write(usage());
350
378
  return;
351
379
  }
380
+ if (!setup && !check && !list && !doctor && !demo && await handOffToConfiguredVersion()) return;
352
381
  if (doctor && demo) throw new Error('Use either --doctor or --demo, not both.');
353
382
  if (demo) {
354
383
  await runOfflineDemo();
@@ -383,14 +412,8 @@ async function main() {
383
412
  detectAdtUrl: detectLocalAdtUrl
384
413
  };
385
414
  if (hasFlag('--npx')) {
386
- // Windows has no npx.exe, so generated entries there launch through
387
- // `cmd /c npx`; other platforms use npx directly.
388
- const launcher = npxMcpLauncher();
389
- setupOptions.install = (selected, options) => installMcpConfig(selected, {
390
- ...options,
391
- command: launcher.command,
392
- args: [...launcher.prefixArgs, '--yes', '--ignore-scripts', `--package=sap-ai-dev-toolkit@${pkg.version}`, 'sap-ai-dev']
393
- });
415
+ const launcher = npxPackageLauncher(pkg.version);
416
+ setupOptions.install = (selected, options) => installMcpConfig(selected, { ...options, ...launcher });
394
417
  }
395
418
  await runSetup(setupOptions);
396
419
  return;
@@ -450,15 +473,7 @@ async function main() {
450
473
  : (/\.(?:cmd|bat)$/i.test(binary)
451
474
  ? { command: 'cmd', prefixArgs: ['/c', binary] }
452
475
  : { command: binary, prefixArgs: [] });
453
- const child = spawn(spawnTarget.command, [...spawnTarget.prefixArgs, ...process.argv.slice(2)], { env: runtimeEnv, stdio: 'inherit' });
454
- process.once('SIGINT', () => child.kill('SIGINT'));
455
- process.once('SIGTERM', () => child.kill('SIGTERM'));
456
- const [code, signal] = await new Promise((resolve, reject) => {
457
- child.once('error', reject);
458
- child.once('exit', (exitCode, exitSignal) => resolve([exitCode, exitSignal]));
459
- });
460
- if (signal) process.kill(process.pid, signal);
461
- process.exitCode = code ?? 1;
476
+ await runOnOwnStdio(spawnTarget.command, [...spawnTarget.prefixArgs, ...process.argv.slice(2)], runtimeEnv);
462
477
  return;
463
478
  }
464
479
 
@@ -131,6 +131,26 @@ export function npxMcpLauncher(platform = process.platform) {
131
131
  : { command: 'npx', prefixArgs: [] };
132
132
  }
133
133
 
134
+ export function npxPackageLauncher(version, platform = process.platform) {
135
+ const { command, prefixArgs } = npxMcpLauncher(platform);
136
+ return { command, args: [...prefixArgs, '--yes', '--ignore-scripts', `--package=sap-ai-dev-toolkit${version ? `@${version}` : ''}`, 'sap-ai-dev'] };
137
+ }
138
+
139
+ function isOlderVersion(version, other) {
140
+ const parts = value => Array.from({ length: 3 }, (_, index) => Number.parseInt(String(value).split('.')[index], 10) || 0);
141
+ const [current, target] = [parts(version), parts(other)];
142
+ const index = current.findIndex((part, position) => part !== target[position]);
143
+ return index >= 0 && current[index] < target[index];
144
+ }
145
+
146
+ // Setup pins its version into each entry, so a stale sap-ai-dev on PATH can hand off
147
+ // to that version through npx instead of running old code; the handoff happens once.
148
+ export function configuredVersionHandoff(env, runningVersion, platform = process.platform) {
149
+ const configured = brandedEnvValue(env, 'VERSION');
150
+ if (!configured || brandedEnvValue(env, 'HANDOFF') || !isOlderVersion(runningVersion, configured)) return null;
151
+ return { version: configured, ...npxPackageLauncher(configured, platform) };
152
+ }
153
+
134
154
  // Normalizes the argument list of npx-launched entries, whether written as
135
155
  // `npx ...` or the Windows `cmd /c npx ...` form; returns null when the entry
136
156
  // is not npx-launched.
@@ -146,17 +166,36 @@ function npxLaunchArgs(entry) {
146
166
  return null;
147
167
  }
148
168
 
169
+ // The package version a PATH sap-ai-dev starts, found through npm's global
170
+ // layouts (Unix bin symlink, Windows .cmd shim); null when the layout is unknown.
171
+ async function commandPackageVersion(command) {
172
+ const target = await realpath(command).catch(() => command);
173
+ for (const manifest of [join(dirname(dirname(target)), 'package.json'), join(dirname(command), 'node_modules', 'sap-ai-dev-toolkit', 'package.json')]) {
174
+ try {
175
+ const pkg = JSON.parse(await readFile(manifest, 'utf8'));
176
+ if (pkg.name === 'sap-ai-dev-toolkit') return pkg.version;
177
+ } catch {
178
+ // Try the other layout.
179
+ }
180
+ }
181
+ return null;
182
+ }
183
+
149
184
  // Resolves how generated destination entries launch the toolkit: the global
150
- // sap-ai-dev command when it is on PATH, otherwise the pinned package
151
- // through npx (routed through cmd /c on Windows) so entries written by a
152
- // pure `npx sap-ai-dev-toolkit --setup` run still start on every host.
185
+ // sap-ai-dev command when it is on PATH and is this version, otherwise the
186
+ // pinned package through npx (routed through cmd /c on Windows) so entries
187
+ // written by a pure `npx sap-ai-dev-toolkit --setup` run still start on every host.
153
188
  async function resolveLauncherInstallCommand(env) {
154
189
  const resolved = await resolveMcpServerCommand(env);
155
- if (resolved !== 'sap-ai-dev') return { command: resolved, args: null };
156
- const { command, prefixArgs } = npxMcpLauncher();
157
190
  const selfVersion = await runningPackageVersion();
158
- const packageSpec = selfVersion ? `sap-ai-dev-toolkit@${selfVersion}` : 'sap-ai-dev-toolkit';
159
- return { command, args: [...prefixArgs, '--yes', '--ignore-scripts', `--package=${packageSpec}`, 'sap-ai-dev'] };
191
+ let warning;
192
+ if (resolved !== 'sap-ai-dev') {
193
+ const installed = await commandPackageVersion(resolved);
194
+ if (installed === null || !selfVersion || installed === selfVersion) return { command: resolved, args: null };
195
+ // A stale global install would keep starting the old launcher and VSP binary.
196
+ warning = `${resolved} is sap-ai-dev-toolkit ${installed}, not ${selfVersion}; MCP entries start sap-ai-dev-toolkit@${selfVersion} through npx instead. Run "npm install -g sap-ai-dev-toolkit@${selfVersion}" to use the global command.`;
197
+ }
198
+ return { ...npxPackageLauncher(selfVersion), warning };
160
199
  }
161
200
 
162
201
  export async function resolveMcpConfigPath(env = process.env) {
@@ -576,12 +615,14 @@ export async function installMcpConfig(destinationsOrOptions, options = {}) {
576
615
  const resolved = await resolveLauncherInstallCommand(env);
577
616
  launcherCommand = resolved.command;
578
617
  launcherArgs = resolved.args;
618
+ if (resolved.warning) installWarnings.push(resolved.warning);
579
619
  }
580
620
  if (launcherCommand) {
581
621
  for (const entry of Object.values(generated)) {
582
622
  if (isCompanionServer(entry)) continue;
583
623
  entry.command = launcherCommand;
584
624
  if (launcherArgs?.length) entry.args = [...launcherArgs];
625
+ if (selfVersion) entry.env.SAP_AI_DEV_TOOLKIT_VERSION = selfVersion;
585
626
  }
586
627
  }
587
628
  const servers = Object.create(null);