sap-ai-dev-toolkit 0.9.0 → 0.9.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/README.md CHANGED
@@ -46,6 +46,12 @@ npm install --global sap-ai-dev-toolkit
46
46
  sap-ai-dev --setup
47
47
  ```
48
48
 
49
+ Or run the whole toolkit once with npx — no global install, nothing left behind except the generated MCP configuration:
50
+
51
+ ```sh
52
+ npx --yes --ignore-scripts sap-ai-dev-toolkit --setup
53
+ ```
54
+
49
55
  To also add optional full-stack SAP companion MCP servers for Fiori, UI5, CAP, and browser validation, run:
50
56
 
51
57
  ```sh
@@ -252,7 +258,16 @@ Install globally from SAP Business Application Studio or from a local workstatio
252
258
  | **Any local interactive terminal, already installed** | `sap-ai-dev --setup` | Re-runs the wizard to add, update, or remove generated MCP entries. Plain `sap-ai-dev` with no arguments also starts local setup when no BAS environment is detected. |
253
259
  | **npm install hook did not show a wizard** | `sap-ai-dev --setup` | This is expected on some npm/Windows combinations because lifecycle scripts may run without an interactive console. The explicit setup command is the reliable path. |
254
260
  | **Force npm to show install-time script output** | `npm install --global --foreground-scripts sap-ai-dev-toolkit` | Optional troubleshooting mode if you specifically want to see postinstall output during installation. Still run `sap-ai-dev --setup` afterwards if no destinations were configured. |
255
- | **No global install / temporary setup** | `npx --yes --ignore-scripts --package=sap-ai-dev-toolkit sap-ai-dev --setup --npx` | Runs the guided setup through npm and writes generated entries that launch the pinned package through `npx`. |
261
+ | **No global install / temporary setup** | `npx --yes --ignore-scripts sap-ai-dev-toolkit --setup` | Runs the guided setup through npm and writes generated entries that launch the pinned package through `npx` (`cmd /c npx` on Windows). Add `--npx` to force the npx launcher even when a global `sap-ai-dev` command exists. |
262
+
263
+ ### 🪟 Windows notes
264
+
265
+ Everything works natively on Windows — PowerShell, cmd, and Windows Terminal are all supported:
266
+
267
+ - `npm install --global sap-ai-dev-toolkit` puts three commands on `PATH`: `sap-ai-dev`, its alias `sap-ai-dev-toolkit`, and `sap-ai-hana`. If `sap-ai-dev` is reported as unknown, npm's global bin directory is missing from `PATH` (run `npm prefix -g` to find it) or the install did not complete — `npx --yes --ignore-scripts sap-ai-dev-toolkit --setup` works without any global install.
268
+ - The pinned VSP runtime ships inside the package as a native `vsp-win32-x64.exe`; no Go toolchain, WSL, or extra downloads are required.
269
+ - MCP entries that launch through npm are written as `cmd /c npx ...` on Windows — the form VS Code, Claude Code, and Cursor can spawn there. Entries written on Linux/macOS use plain `npx`.
270
+ - Local SAP GUI discovery reads the Windows landscape files and registry, and setup can write an experimental Windows SSO (`Negotiate`/SPNEGO) entry; see the “Local machine SAP GUI support” section below.
256
271
 
257
272
  If you previously installed the old package, remove it first because the legacy `bas-vsp-mcp` command is no longer shipped:
258
273
 
@@ -312,9 +327,9 @@ The same postinstall offers the bundled agents and skills for several AI coding
312
327
 
313
328
  Some current npm versions also require install hooks to be approved. If npm reports that `sap-ai-dev-toolkit`'s `postinstall` was blocked, allow it during a fresh install with `npm install --global --allow-scripts=sap-ai-dev-toolkit sap-ai-dev-toolkit`, or run `sap-ai-dev --setup` from an interactive terminal after installation.
314
329
 
315
- The setup report uses icons and terminal colors; set `NO_COLOR=1` to disable ANSI colors. 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.
330
+ 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.
316
331
 
317
- 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. Use `--npx` to make generated destination entries start the pinned package through npm instead of relying on a global `sap-ai-dev` command; companion tools already use `npx` with their own npm packages.
332
+ 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.
318
333
 
319
334
  If a setup step is skipped or fails, rerun it from an interactive terminal:
320
335
 
@@ -325,10 +340,10 @@ sap-ai-dev --setup
325
340
  To configure without a global install, run the guided setup directly through npm:
326
341
 
327
342
  ```sh
328
- npx --yes --ignore-scripts --package=sap-ai-dev-toolkit sap-ai-dev --setup --npx
343
+ npx --yes --ignore-scripts sap-ai-dev-toolkit --setup
329
344
  ```
330
345
 
331
- This lists discovered systems in the same checkbox picker, with nothing selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). It writes MCP entries that launch the selected servers through npx. The entries pin the package version used during setup, and `--ignore-scripts` avoids running the install-time wizard a second time. After setup, in BAS or VS Code run **MCP: List Servers**, select each chosen destination, and choose **Start Server**.
346
+ This lists discovered systems in the same checkbox picker, with nothing selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). Because no global `sap-ai-dev` command is on `PATH`, it writes MCP entries that launch the pinned package version through npx — through `cmd /c npx` on Windows, which is the form VS Code, Claude Code, and Cursor can spawn there. `--ignore-scripts` avoids running the install-time wizard a second time; add `--npx` to force this launcher shape even when the toolkit is installed globally. After setup, in BAS or VS Code run **MCP: List Servers**, select each chosen destination, and choose **Start Server**.
332
347
 
333
348
  For a non-interactive installation or a platform without a published VSP asset, provide a trusted binary override:
334
349
 
@@ -423,7 +438,7 @@ Review skipped or conflicting files and merge changes manually. `LintABAP` analy
423
438
  If the `cf` CLI is version 8.18 or newer and authenticated to a targeted space, setup offers an opt-in import from that space's SAP Destination service. It does not create service keys unless you accept. The imported records are limited to the current space and are merged with BAS destinations for selection. OnPremise destinations require choosing a Connectivity service instance; runtime traffic uses that service's proxy, and PrincipalPropagation uses the current CF user's token. Internet destinations use the configured HTTP(S) proxy environment. Setup stores only service-instance/key references in `mcp.json`, not the service-key credentials. At runtime the generated entry verifies the active CF space and resolves those references. Setup removes an add-on-created key only when no remaining CF entry references it and the CLI is targeted to the key's recorded space; keys are left untouched when the space cannot be verified.
424
439
  Cloud Foundry HTTP destination probes use forward-form HTTP requests, not CONNECT tunnels; SAP Connectivity requires HTTP from the application to its proxy. OnPremise requests are canceled when the client disconnects or the runtime route closes.
425
440
 
426
- Interactive npm install and `sap-ai-dev --setup` both use a checkbox picker with no destinations selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). Confirming with none checked removes this package's generated MCP entries. Non-interactive installs skip destination or local SAP GUI system selection without changing MCP config. Run the interactive setup command above; add `--npx` when the package is not installed globally.
441
+ Interactive npm install and `sap-ai-dev --setup` both use a checkbox picker with no destinations selected by default. Use **Space** to choose destinations, **Enter** to confirm, and **a** to toggle all (select all if any are unchecked; otherwise clear the selection). Confirming with none checked removes this package's generated MCP entries. Non-interactive installs skip destination or local SAP GUI system selection without changing MCP config. Run the interactive setup command above; generated entries fall back to the pinned npx launcher automatically when the package is not installed globally.
427
442
 
428
443
  Setup tidies its own footprint: it reconciles MCP entries managed by this package and removes legacy `sapAiDev_*` / `basVspMcp_*` entries from earlier releases. Existing unrelated MCP servers and top-level configuration such as `inputs` stay untouched.
429
444
 
@@ -857,6 +872,8 @@ NPM_PUBLISH_TOKEN=npm_...
857
872
  - To publish a patch bump, run `npm run publish:npm -- --patch`; it updates `package.json` and `package-lock.json` before publishing. If publishing fails after the bump, retry without `--patch`.
858
873
  - npm's `prepublishOnly` hook builds the pinned VSP binaries for all platforms (including the Go toolchain if missing) and refuses to ship a tarball whose `dist/checksums.txt` does not list all five assets; it guards `npm run publish:npm` and a plain `npm publish` alike. Pass `--skip-build` to republish an already-built `dist/` without rebuilding.
859
874
  - Preview the package without publishing, changing its version, or building with `npm run publish:npm -- --dry-run`.
875
+ - GitHub Actions runs the test suite on pushes to `main` and pull requests. A push to `main` that changes the `package.json` version runs the same checks, builds the VSP binaries, publishes that version to npm, and creates a `v<version>` GitHub release with generated notes.
876
+ - The publish workflow uses the `NPM_PUBLISH_TOKEN` GitHub repository secret. Verify it with `gh secret list`; the token value is never stored in the repository.
860
877
 
861
878
  The token is not printed or stored in the repository.
862
879
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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",
@@ -17,6 +17,7 @@
17
17
  },
18
18
  "bin": {
19
19
  "sap-ai-dev": "src/launcher.mjs",
20
+ "sap-ai-dev-toolkit": "src/launcher.mjs",
20
21
  "sap-ai-hana": "src/hana-inspector.mjs"
21
22
  },
22
23
  "scripts": {
@@ -9,7 +9,7 @@ import { installMcpServersForHarnesses } from '../src/harness-mcp-config.mjs';
9
9
  import { DEFAULT_HARNESS_IDS, HARNESSES, HARNESS_ENV_VAR, NON_INTERACTIVE_HARNESS_IDS, envHarnessSelection, harnessRoot } from '../src/harnesses.mjs';
10
10
  import { ReadStream as TTYReadStream, WriteStream as TTYWriteStream } from 'node:tty';
11
11
  import { homedir } from 'node:os';
12
- import { checkboxPrompt, colorText, formatStatus } from '../src/terminal-ui.mjs';
12
+ import { checkboxPrompt, colorText, formatStatus, startProgress } from '../src/terminal-ui.mjs';
13
13
  import { brandedEnvValue, withBrandedEnvironment } from '../src/branding.mjs';
14
14
  import { generatedServerName } from '../src/mcp-config.mjs';
15
15
  import { redactText } from '../src/redact.mjs';
@@ -232,7 +232,7 @@ async function announceSetup(result) {
232
232
  return;
233
233
  }
234
234
  if (result?.reason === 'non-tty') {
235
- await announce('Destination selection was skipped because npm did not provide an interactive terminal.\nMCP config was not changed.\nRun sap-ai-dev --setup from an interactive BAS terminal, or run npx --yes --ignore-scripts --package=sap-ai-dev-toolkit sap-ai-dev --setup --npx.', 'warning');
235
+ await announce('Destination selection was skipped because npm did not provide an interactive terminal.\nMCP config was not changed.\nRun sap-ai-dev --setup from an interactive terminal, or run npx --yes --ignore-scripts sap-ai-dev-toolkit --setup from any terminal.', 'warning');
236
236
  return;
237
237
  }
238
238
  if (result?.reason === 'no-destinations') {
@@ -304,11 +304,15 @@ async function main() {
304
304
  if (brandedEnvValue(runtimeEnv, 'BINARY')) {
305
305
  await announce('Using the SAP_AI_DEV_TOOLKIT_BINARY override.', 'info');
306
306
  } else {
307
- await announce('Preparing the pinned VSP runtime for this platform.', 'progress');
307
+ // The bundled binary resolves in milliseconds; the animation only
308
+ // appears when provisioning actually takes time (download fallback).
309
+ const stopBinaryProgress = startProgress('Preparing the pinned VSP runtime for this platform', { output: process.stderr, env: runtimeEnv });
308
310
  try {
309
311
  await installBinary(pkg, { env: runtimeEnv });
312
+ stopBinaryProgress();
310
313
  await announce('Pinned VSP binary installed and ready.', 'success');
311
314
  } catch (error) {
315
+ stopBinaryProgress();
312
316
  throw new Error(`VSP binary provisioning failed: ${error.message}`);
313
317
  }
314
318
  }
package/src/launcher.mjs CHANGED
@@ -7,13 +7,14 @@ import { discoverDestinations, remediation, slugifyDestination, statusRows } fro
7
7
  import { discoverSapGuiSystems } from './local-sap-gui.mjs';
8
8
  import { findBinary } from './binary.mjs';
9
9
  import { MCPProxy } from './mcp-proxy.mjs';
10
- import { installMcpConfig, repairManagedMcpConfig } from './mcp-config.mjs';
10
+ import { installMcpConfig, npxMcpLauncher, repairManagedMcpConfig } from './mcp-config.mjs';
11
11
  import { runSetup } from './setup.mjs';
12
12
  import { resolveConfiguredCloudFoundryDestination } from './cf-destination.mjs';
13
13
  import { createWindowsSsoAdtProxy } from './windows-sso-adt-proxy.mjs';
14
14
  import { createTlsServerNameAdtProxy } from './tls-adt-proxy.mjs';
15
15
  import { withBrandedEnvironment } from './branding.mjs';
16
16
  import { redactText } from './redact.mjs';
17
+ import { startProgress } from './terminal-ui.mjs';
17
18
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
18
19
  const pkg = JSON.parse(await readFile(join(root, 'package.json'), 'utf8'));
19
20
  const runtimeEnv = withBrandedEnvironment(process.env);
@@ -53,7 +54,9 @@ function doctorRow(name, stage, status, detail) {
53
54
 
54
55
  async function runDoctor(destinations) {
55
56
  const checks = [];
57
+ const stopProgress = startProgress(destinations.length === 1 ? `Checking ${destinations[0]?.name || 'destination'}` : `Checking ${destinations.length} destinations`, { output: process.stderr, label: 'Doctor' });
56
58
  if (!destinations.length) {
59
+ stopProgress();
57
60
  return { ok: false, destinations: 0, checks: [doctorRow('-', 'destination discovery', 'failed', 'No destinations were found. Check H2O_URL or the configured Cloud Foundry destination.')] };
58
61
  }
59
62
  let binary;
@@ -76,6 +79,7 @@ async function runDoctor(destinations) {
76
79
  checks.push(doctorRow('-', 'MCP config repair', 'failed', redactText(error.message || error).slice(0, 300)));
77
80
  }
78
81
  for (const destination of destinations) {
82
+ stopProgress.update(`Checking ${destination.name}: ADT probe, VSP startup, tools/list`);
79
83
  const probe = destination.probe;
80
84
  const probeStatus = !probe || probe.status === 'skipped' ? 'skipped' : (probe.available === true ? 'passed' : 'failed');
81
85
  checks.push(doctorRow(destination.name, 'ADT probe', probeStatus, probe?.status || 'No BAS ADT probe was supplied; GetSystemInfo will check the route.'));
@@ -126,6 +130,7 @@ async function runDoctor(destinations) {
126
130
  }
127
131
  }
128
132
  const required = checks.filter(check => check.status !== 'skipped');
133
+ stopProgress();
129
134
  return { ok: required.every(check => check.status === 'passed'), destinations: destinations.length, checks };
130
135
  }
131
136
 
@@ -253,11 +258,17 @@ async function main() {
253
258
  if (doctor) {
254
259
  let report;
255
260
  try {
256
- const destinations = runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry'
257
- ? [await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })]
258
- : (runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'sap-gui-local'
259
- ? [await configuredLocalSapGuiDestination(runtimeEnv)]
260
- : await discoverForCommand());
261
+ const stopDiscoveryProgress = startProgress('Discovering SAP destinations', { output: process.stderr, env: runtimeEnv, label: 'Doctor' });
262
+ let destinations;
263
+ try {
264
+ destinations = runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'cloud-foundry'
265
+ ? [await resolveConfiguredCloudFoundryDestination({ env: runtimeEnv })]
266
+ : (runtimeEnv.SAP_AI_DEV_TOOLKIT_DESTINATION_SOURCE === 'sap-gui-local'
267
+ ? [await configuredLocalSapGuiDestination(runtimeEnv)]
268
+ : await discoverForCommand());
269
+ } finally {
270
+ stopDiscoveryProgress();
271
+ }
261
272
  report = await runDoctor(destinations);
262
273
  } catch (error) {
263
274
  report = { ok: false, destinations: 0, checks: [doctorRow('-', 'destination discovery', 'failed', redactText(error.message || error).slice(0, 300))] };
@@ -272,17 +283,26 @@ async function main() {
272
283
  includeSapDevelopmentToolsPrompt: hasFlag('--tools') || hasFlag('--companion-tools')
273
284
  };
274
285
  if (hasFlag('--npx')) {
286
+ // Windows has no npx.exe, so generated entries there launch through
287
+ // `cmd /c npx`; other platforms use npx directly.
288
+ const launcher = npxMcpLauncher();
275
289
  setupOptions.install = (selected, options) => installMcpConfig(selected, {
276
290
  ...options,
277
- command: 'npx',
278
- args: ['--yes', '--ignore-scripts', `--package=sap-ai-dev-toolkit@${pkg.version}`, 'sap-ai-dev']
291
+ command: launcher.command,
292
+ args: [...launcher.prefixArgs, '--yes', '--ignore-scripts', `--package=sap-ai-dev-toolkit@${pkg.version}`, 'sap-ai-dev']
279
293
  });
280
294
  }
281
295
  await runSetup(setupOptions);
282
296
  return;
283
297
  }
284
298
  if (check || list) {
285
- const destinations = await discoverForCommand();
299
+ const stopProgress = startProgress('Discovering SAP destinations and probing ADT endpoints', { output: process.stderr, env: runtimeEnv });
300
+ let destinations;
301
+ try {
302
+ destinations = await discoverForCommand();
303
+ } finally {
304
+ stopProgress();
305
+ }
286
306
  const statuses = statusRows(destinations);
287
307
  if (list && hasFlag('--json')) json(statuses);
288
308
  else for (const row of statuses) console.error(`${row.name}: client=${row.client} authentication=${row.authentication} probe=${row.probe}`);
@@ -319,11 +339,15 @@ async function main() {
319
339
  return;
320
340
  }
321
341
  const binary = await binaryOrError();
322
- // Windows cannot exec a .js/.mjs file directly; route JavaScript entries
342
+ // Windows cannot exec .js/.mjs files directly; route JavaScript entries
323
343
  // (test fixtures) through the current Node binary like MCPProxy does.
344
+ // Windows .cmd/.bat shims (e.g. a SAP_AI_DEV_TOOLKIT_BINARY override)
345
+ // must go through cmd /c; Node refuses to spawn batch files directly.
324
346
  const spawnTarget = /\.(?:mjs|cjs|js)$/i.test(binary)
325
347
  ? { command: process.execPath, prefixArgs: [binary] }
326
- : { command: binary, prefixArgs: [] };
348
+ : (/\.(?:cmd|bat)$/i.test(binary)
349
+ ? { command: 'cmd', prefixArgs: ['/c', binary] }
350
+ : { command: binary, prefixArgs: [] });
327
351
  const child = spawn(spawnTarget.command, [...spawnTarget.prefixArgs, ...process.argv.slice(2)], { env: runtimeEnv, stdio: 'inherit' });
328
352
  process.once('SIGINT', () => child.kill('SIGINT'));
329
353
  process.once('SIGTERM', () => child.kill('SIGTERM'));
@@ -336,7 +360,13 @@ async function main() {
336
360
  return;
337
361
  }
338
362
 
339
- const discovered = await discoverDestinations({ env: runtimeEnv });
363
+ const stopProgress = startProgress('Discovering BAS destinations and probing ADT endpoints', { output: process.stderr, env: runtimeEnv });
364
+ let discovered;
365
+ try {
366
+ discovered = await discoverDestinations({ env: runtimeEnv });
367
+ } finally {
368
+ stopProgress();
369
+ }
340
370
  for (const destination of discovered) logLine(probeDiagnostic(destination));
341
371
  const destinations = discovered;
342
372
  if (!destinations.length) {
@@ -98,6 +98,44 @@ export async function resolveMcpServerCommand(env = process.env) {
98
98
  return 'sap-ai-dev';
99
99
  }
100
100
 
101
+ // Windows ships npx as npx.cmd, not npx.exe: MCP hosts spawn the configured
102
+ // command directly without shell PATH resolution, so npx-launched servers
103
+ // must route through `cmd /c npx` there (the form VS Code, Claude Code, and
104
+ // Cursor document for Windows). Other platforms run npx directly.
105
+ export function npxMcpLauncher(platform = process.platform) {
106
+ return platform === 'win32'
107
+ ? { command: 'cmd', prefixArgs: ['/c', 'npx'] }
108
+ : { command: 'npx', prefixArgs: [] };
109
+ }
110
+
111
+ // Normalizes the argument list of npx-launched entries, whether written as
112
+ // `npx ...` or the Windows `cmd /c npx ...` form; returns null when the entry
113
+ // is not npx-launched.
114
+ function npxLaunchArgs(entry) {
115
+ const args = Array.isArray(entry?.args) ? entry.args.map(String) : [];
116
+ if (entry?.command === 'npx') return args;
117
+ const commandName = typeof entry?.command === 'string'
118
+ ? entry.command.slice(Math.max(entry.command.lastIndexOf('/'), entry.command.lastIndexOf('\\')) + 1)
119
+ : '';
120
+ if (/^cmd(?:\.exe)?$/i.test(commandName) && args[0]?.toLowerCase() === '/c' && /^npx(?:\.cmd)?$/i.test(args[1] || '')) {
121
+ return args.slice(2);
122
+ }
123
+ return null;
124
+ }
125
+
126
+ // Resolves how generated destination entries launch the toolkit: the global
127
+ // sap-ai-dev command when it is on PATH, otherwise the pinned package
128
+ // through npx (routed through cmd /c on Windows) so entries written by a
129
+ // pure `npx sap-ai-dev-toolkit --setup` run still start on every host.
130
+ async function resolveLauncherInstallCommand(env) {
131
+ const resolved = await resolveMcpServerCommand(env);
132
+ if (resolved !== 'sap-ai-dev') return { command: resolved, args: null };
133
+ const { command, prefixArgs } = npxMcpLauncher();
134
+ const selfVersion = await runningPackageVersion();
135
+ const packageSpec = selfVersion ? `sap-ai-dev-toolkit@${selfVersion}` : 'sap-ai-dev-toolkit';
136
+ return { command, args: [...prefixArgs, '--yes', '--ignore-scripts', `--package=${packageSpec}`, 'sap-ai-dev'] };
137
+ }
138
+
101
139
  export async function resolveMcpConfigPath(env = process.env) {
102
140
  const configuredPath = [env.SAP_AI_DEV_MCP_CONFIG, brandedEnvValue(env, 'MCP_CONFIG')]
103
141
  .find(value => typeof value === 'string' && value.length > 0);
@@ -138,16 +176,17 @@ export function generatedDestinationServerName(destination) {
138
176
  return generatedServerName(destination?.source === 'cloud-foundry' ? destination.serverName : destination?.name);
139
177
  }
140
178
 
141
- export function buildSapDevelopmentMcpEntries(serverIds = SAP_DEVELOPMENT_MCP_SERVERS.map(server => server.id), { packageVersions = {}, packageManager = 'npx' } = {}) {
179
+ export function buildSapDevelopmentMcpEntries(serverIds = SAP_DEVELOPMENT_MCP_SERVERS.map(server => server.id), { packageVersions = {}, packageManager = 'npx', platform = process.platform } = {}) {
142
180
  const selected = new Set(serverIds);
143
181
  const entries = Object.create(null);
182
+ const launcher = packageManager === 'npx' ? npxMcpLauncher(platform) : { command: packageManager, prefixArgs: [] };
144
183
  for (const server of SAP_DEVELOPMENT_MCP_SERVERS) {
145
184
  if (!selected.has(server.id)) continue;
146
185
  const packageSpec = packageVersions[server.id] || packageVersions[server.packageName] || server.packageName;
147
186
  entries[server.id] = {
148
187
  type: 'stdio',
149
- command: packageManager,
150
- args: ['--yes', ...(server.ignoreScripts ? ['--ignore-scripts'] : []), `--package=${packageSpec}`, server.bin],
188
+ command: launcher.command,
189
+ args: [...launcher.prefixArgs, '--yes', ...(server.ignoreScripts ? ['--ignore-scripts'] : []), `--package=${packageSpec}`, server.bin],
151
190
  BAS_EXT: 'true',
152
191
  BAS_EXT_KIND: COMPANION_SERVER_KIND,
153
192
  displayName: server.name,
@@ -232,16 +271,14 @@ function isPackageLauncher(entry) {
232
271
  const legacyCommand = legacyCommands.has(entry?.command) || legacyCommands.has(commandName);
233
272
  const currentPackages = [/^--package=sap-ai-dev-toolkit(?:@[^/]+)?$/];
234
273
  const legacyPackages = [/^--package=bas-mcp-addon(?:@[^/]+)?$/];
235
- const hasPackage = patterns => Array.isArray(entry?.args)
236
- && entry.args.some(argument => typeof argument === 'string' && patterns.some(pattern => pattern.test(argument)));
237
- const currentNpxLauncher = entry?.command === 'npx'
238
- && Array.isArray(entry.args)
239
- && entry.args.some(argument => currentCommands.has(argument))
240
- && hasPackage(currentPackages);
241
- const legacyNpxLauncher = entry?.command === 'npx'
242
- && Array.isArray(entry.args)
243
- && entry.args.some(argument => legacyCommands.has(argument))
244
- && hasPackage(legacyPackages);
274
+ const hasPackage = (arguments_, patterns) => arguments_.some(argument => patterns.some(pattern => pattern.test(argument)));
275
+ const npxArgs = npxLaunchArgs(entry);
276
+ const currentNpxLauncher = npxArgs !== null
277
+ && npxArgs.some(argument => currentCommands.has(argument))
278
+ && hasPackage(npxArgs, currentPackages);
279
+ const legacyNpxLauncher = npxArgs !== null
280
+ && npxArgs.some(argument => legacyCommands.has(argument))
281
+ && hasPackage(npxArgs, legacyPackages);
245
282
  // Current launchers are treated as managed only when tagged by this add-on.
246
283
  // Some early bas-mcp-addon entries were not tagged with BAS_EXT, though, and
247
284
  // leaving them behind keeps advertising the old full VSP surface (~159 tools).
@@ -264,10 +301,10 @@ function isManagedBasDestinationEntry(entry) {
264
301
  ? entry.command.slice(Math.max(entry.command.lastIndexOf('/'), entry.command.lastIndexOf('\\')) + 1).replace(/\.(?:cmd|exe)$/i, '')
265
302
  : '';
266
303
  const currentCommand = currentCommands.has(entry?.command) || currentCommands.has(commandName);
267
- const currentNpxLauncher = entry?.command === 'npx'
268
- && Array.isArray(entry.args)
269
- && entry.args.some(argument => currentCommands.has(argument))
270
- && entry.args.some(argument => typeof argument === 'string' && /^--package=sap-ai-dev-toolkit(?:@[^/]+)?$/.test(argument));
304
+ const npxArgs = npxLaunchArgs(entry);
305
+ const currentNpxLauncher = npxArgs !== null
306
+ && npxArgs.some(argument => currentCommands.has(argument))
307
+ && npxArgs.some(argument => /^--package=sap-ai-dev-toolkit(?:@[^/]+)?$/.test(argument));
271
308
  return entry?.BAS_EXT === 'true'
272
309
  && (currentCommand || currentNpxLauncher)
273
310
  && typeof destinationValue(entry?.env) === 'string'
@@ -356,12 +393,18 @@ export async function installMcpConfig(destinationsOrOptions, options = {}) {
356
393
  if (input?.id) generatedInputs.set(input.id, input);
357
394
  }
358
395
  }
359
- const launcherCommand = options.command || (destinations.length ? await resolveMcpServerCommand(env) : null);
396
+ let launcherCommand = options.command || null;
397
+ let launcherArgs = options.command ? options.args : null;
398
+ if (!launcherCommand && destinations.length) {
399
+ const resolved = await resolveLauncherInstallCommand(env);
400
+ launcherCommand = resolved.command;
401
+ launcherArgs = resolved.args;
402
+ }
360
403
  if (launcherCommand) {
361
404
  for (const entry of Object.values(generated)) {
362
405
  if (isCompanionServer(entry)) continue;
363
406
  entry.command = launcherCommand;
364
- if (options.command && options.args?.length) entry.args = [...options.args];
407
+ if (launcherArgs?.length) entry.args = [...launcherArgs];
365
408
  }
366
409
  }
367
410
  const servers = Object.create(null);
@@ -406,7 +449,7 @@ export async function repairManagedMcpConfig(discoveredDestinations, { env = pro
406
449
  const config = await readConfig(configPath);
407
450
  const managedEntries = Object.values(config.servers).filter(isManagedBasDestinationEntry).length;
408
451
  if (!basDestinations.size) return { path: configPath, changed: false, repaired: 0, managedEntries };
409
- const localCommand = await resolveMcpServerCommand(env);
452
+ const localLauncher = await resolveLauncherInstallCommand(env);
410
453
  let repaired = 0;
411
454
  for (const [serverName, entry] of Object.entries(config.servers)) {
412
455
  if (!isManagedBasDestinationEntry(entry)) continue;
@@ -430,14 +473,16 @@ export async function repairManagedMcpConfig(discoveredDestinations, { env = pro
430
473
  delete nextEnvironment.BAS_VSP_DESTINATION;
431
474
  delete nextEnvironment.BAS_VSP_DESTINATION_SOURCE;
432
475
  const nextEntry = { ...entry, type: 'stdio', env: nextEnvironment, BAS_EXT: 'true' };
433
- if (entry.command === 'npx') {
476
+ if (npxLaunchArgs(entry) !== null) {
434
477
  if (packageVersion && Array.isArray(entry.args)) {
435
478
  nextEntry.args = entry.args.map(argument => typeof argument === 'string' && /^--package=sap-ai-dev-toolkit(?:@[^/]+)?$/.test(argument)
436
479
  ? `--package=sap-ai-dev-toolkit@${packageVersion}`
437
480
  : argument);
438
481
  }
439
482
  } else {
440
- nextEntry.command = localCommand;
483
+ nextEntry.command = localLauncher.command;
484
+ if (localLauncher.args) nextEntry.args = [...localLauncher.args];
485
+ else delete nextEntry.args;
441
486
  }
442
487
  if (expectedName === serverName) {
443
488
  if (JSON.stringify(nextEntry) === JSON.stringify(entry)) continue;
package/src/mcp-proxy.mjs CHANGED
@@ -200,9 +200,12 @@ export function childArguments(destination, env = process.env) {
200
200
 
201
201
  // Windows cannot exec a .js/.mjs file directly (no shebang support), so test
202
202
  // fixtures written in JavaScript are launched through the current Node binary.
203
- // The production VSP binary is a native executable and is unaffected.
203
+ // The production VSP binary is a native executable and is unaffected; a
204
+ // Windows .cmd/.bat override needs cmd /c because Node refuses to spawn
205
+ // batch files directly (EINVAL since the CVE-2024-27980 hardening).
204
206
  function spawnCommand(binary) {
205
207
  if (/\.(?:mjs|cjs|js)$/i.test(binary)) return [process.execPath, binary];
208
+ if (/\.(?:cmd|bat)$/i.test(binary) && process.platform === 'win32') return ['cmd', '/c', binary];
206
209
  return [binary];
207
210
  }
208
211
 
package/src/setup.mjs CHANGED
@@ -2,7 +2,7 @@ import { createInterface } from 'node:readline/promises';
2
2
  import { stdin, stdout } from 'node:process';
3
3
  import { platform } from 'node:os';
4
4
  import { isIP } from 'node:net';
5
- import { checkboxPrompt, colorText, formatStatus, selectPrompt, textPrompt } from './terminal-ui.mjs';
5
+ import { checkboxPrompt, colorText, formatStatus, selectPrompt, startProgress, textPrompt } from './terminal-ui.mjs';
6
6
  import { discoverDestinations, remediation } from './bas-discovery.mjs';
7
7
  import { defaultAdtUrl, discoverSapGuiSystems } from './local-sap-gui.mjs';
8
8
  import { discoverCloudFoundryDestinations, getCloudFoundryTarget, deleteManagedCloudFoundryServiceKeys, findOrphanedCloudFoundryServiceKeys } from './cf-destination.mjs';
@@ -237,15 +237,21 @@ export async function runSetup({
237
237
  return { skipped: true, reason: 'non-tty' };
238
238
  }
239
239
  print(output, '');
240
- print(output, formatStatus(isBas ? 'Contacting BAS to discover destinations; this may take a moment.' : 'Looking for local SAP GUI system configuration.', 'progress', output, 'Setup'));
240
+ if (!isBas) print(output, formatStatus('Looking for local SAP GUI system configuration.', 'progress', output, 'Setup'));
241
241
 
242
242
  let destinations = [];
243
243
  if (isBas) {
244
244
  let basDestinations;
245
+ // BAS discovery probes every destination's ADT endpoint (up to a 5s
246
+ // timeout each); the animated progress line keeps the terminal visibly
247
+ // alive from the first moment instead of looking stuck.
248
+ const stopDiscoveryProgress = startProgress('Contacting BAS to discover destinations', { output, env, label: 'Setup' });
245
249
  try {
246
250
  basDestinations = await discover({ env: { ...env, SAP_AI_DEV_TOOLKIT_DESTINATION: '' } });
247
251
  } catch (error) {
248
252
  throw new Error(`BAS discovery failed: ${error.message}`);
253
+ } finally {
254
+ stopDiscoveryProgress();
249
255
  }
250
256
  destinations = basDestinations.map(safeBasDestination);
251
257
  } else {
@@ -401,3 +401,43 @@ export async function textPrompt({ message, secret = false, required = true, pla
401
401
  }
402
402
  });
403
403
  }
404
+
405
+ // Animated progress line for long-running steps (destination discovery,
406
+ // ADT probes, VSP binary provisioning). TTY outputs get spinner frames on an
407
+ // unref'd timer that starts after a short delay, so steps that finish quickly
408
+ // never flash anything. Non-TTY outputs (pipes, MCP hosts, --json callers,
409
+ // CI) stay completely silent so machine consumers never see progress noise.
410
+ // Returns stop(); on a TTY, stop() clears the line so it never pollutes
411
+ // scrollback. stop.update(message) retargets the label while running (for
412
+ // example per destination during --doctor).
413
+ export function startProgress(initialMessage, { output = process.stdout, env = process.env, label = 'sap-ai-dev-toolkit' } = {}) {
414
+ const noop = Object.assign(() => {}, { update: () => {} });
415
+ if (!output || typeof output.write !== 'function') return noop;
416
+ const animationDisabled = env.TERM === 'dumb' || env.CI || env.SAP_AI_DEV_TOOLKIT_DISABLE_SCAN_ANIMATION === 'true';
417
+ if (!output.isTTY || !Number.isFinite(output.columns) || animationDisabled) return noop;
418
+ let message = String(initialMessage);
419
+ let index = 0;
420
+ let active = false;
421
+ let interval;
422
+ const render = () => {
423
+ active = true;
424
+ index += 1;
425
+ const columns = Math.max(20, output.columns || 80);
426
+ const dots = `.${'.'.repeat(index % 3)}${' '.repeat(2 - index % 3)}`;
427
+ const line = `${label}: ${message} ${dots}`.slice(0, Math.max(1, columns - 1));
428
+ output.write(`\r\u001b[2K${line}`);
429
+ };
430
+ const delay = setTimeout(() => {
431
+ render();
432
+ interval = setInterval(render, 120);
433
+ if (typeof interval.unref === 'function') interval.unref();
434
+ }, 150);
435
+ if (typeof delay.unref === 'function') delay.unref();
436
+ const stop = () => {
437
+ clearTimeout(delay);
438
+ if (interval) clearInterval(interval);
439
+ if (active) output.write('\r\u001b[2K');
440
+ };
441
+ stop.update = next => { message = String(next); };
442
+ return stop;
443
+ }