sap-ai-dev-toolkit 0.9.9914 → 0.9.9915

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.
@@ -23,7 +23,7 @@ The Integration Suite companion is never destination-prefixed; its tools are alr
23
23
  - APIM products and key value maps: `apim_list_api_products`, `apim_get_api_product`, `apim_create_api_product`, `apim_update_api_product`, `apim_list_key_value_maps`, `apim_get_key_value_map`, `apim_create_key_value_map`, `apim_set_key_value_map_entries`.
24
24
  - Anything else (access policies, JMS queues, applications, developers, certificate store references): the read-only `cpi_odata_get` / `apim_odata_get`; start with path `/$metadata` to see the entity sets.
25
25
 
26
- `cpi_*` tools appear only when CPI credentials are configured and `apim_*` tools only when APIM credentials are configured. Start every session with `integration_connection_info` and `verify: true` to see which side is available, which tenant hosts are bound, and which tool areas the service key lacks a role for. When a side is missing or cannot sign in, ask the user to run `sap-ai-dev --setup-integration` in a terminal (paste, online check, user-only storage) instead of pasting a key into the chat.
26
+ `cpi_*` tools appear only when CPI credentials are configured and `apim_*` tools only when APIM credentials are configured. Start every session with `integration_connection_info` and `verify: true` to see which side is available, which tenant hosts are bound, and which tool areas the service key lacks a role for. When a side is missing, cannot sign in, or lacks roles, ask the user to run `sap-ai-dev --setup-integration` in a terminal: it takes the key from the logged-in Cloud Foundry space (and can create a dedicated instance with the needed roles) or from a pasted key, checks it online, and stores it user-only. Never ask for a key in the chat.
27
27
 
28
28
  ## Exposing an SAP OData service through API Management
29
29
 
package/README.md CHANGED
@@ -232,8 +232,9 @@ Every interactive `sap-ai-dev --setup` asks **"Add the SAP Integration Suite too
232
232
  To set up only this server, run **`sap-ai-dev --setup-integration`**. In both routes the credential step:
233
233
 
234
234
  1. **Checks detected credentials online.** It signs in and runs one read-only request per tool area (packages, deployed artifacts, message logs, security material, number ranges, Partner Directory, data stores, JMS; API proxies, providers, products, key value maps, virtual hosts, and policy readiness, which exports an existing API proxy bundle the way `apim_attach_api_proxy_policies` does). Each area is marked ✓, ✗ (missing role) or – (not available on the tenant or plan, or no proxy exists yet to test the export with).
235
- 2. **Lets you paste a service key.** Paste a CPI key (Process Integration Runtime, plan `api`) or an APIM key (API Management, plan `apiportal-apiaccess`), either as JSON (multi-line paste works; input is hidden) or as the path to the downloaded `.json` file. The key is verified the same way before it is saved.
236
- 3. **Saves only what the server needs.** The URL, token URL, client id and secret go to `~/.config/sap-ai-dev-toolkit/integration-suite.json`, readable only by you. The secret is never printed and never written to `mcp.json`. A key that cannot sign in is saved only if you confirm.
235
+ 2. **Uses the logged-in Cloud Foundry space (default when `cf` is logged in).** It finds the Process Integration Runtime instance with plan `api` (`it-rt`) and the API Management instance with plan `apiportal-apiaccess`, lets you choose when there are several, and reuses or creates one service key named `sap-ai-dev-integration` on it. The key is then verified and saved like a pasted one. Roles are fixed on the instance, so when the key signs in but lacks roles, or a side has no instance, the wizard offers to create a dedicated instance (`sap-ai-dev-integration-cpi` with `AuthGroup_Administrator`, `AuthGroup_BusinessExpert`, `AuthGroup_IntegrationDeveloper`, `AuthGroup_TenantPartnerDirectoryConfigurator`, `AccessAllAccessPoliciesArtifacts`; `sap-ai-dev-integration-apim` with `APIPortal.Administrator`). It never does this without your `y`.
236
+ 3. **Lets you paste a service key.** Paste a CPI key (Process Integration Runtime, plan `api`) or an APIM key (API Management, plan `apiportal-apiaccess`), either as JSON (multi-line paste works; input is hidden) or as the path to the downloaded `.json` file. The key is verified the same way before it is saved.
237
+ 4. **Saves only what the server needs.** The URL, token URL, client id and secret go to `~/.config/sap-ai-dev-toolkit/integration-suite.json`, readable only by you. The secret is never printed and never written to `mcp.json`. A key that cannot sign in is saved only if you confirm.
237
238
 
238
239
  Environment credentials take precedence over a saved key. Re-check at any time with `sap-ai-integration --check`, or ask the agent to call `integration_connection_info` with `verify: true`. Configure CPI, APIM, or both; per side the first match wins:
239
240
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.9.9914",
3
+ "version": "0.9.9915",
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",
@@ -26,7 +26,7 @@ function cliError(error) {
26
26
  return failure;
27
27
  }
28
28
 
29
- async function runCf(args, { env = process.env, execFileImpl = nodeExecFile } = {}) {
29
+ export async function runCf(args, { env = process.env, execFileImpl = nodeExecFile } = {}) {
30
30
  try {
31
31
  const run = execFileImpl === nodeExecFile ? execFile : promisify(execFileImpl);
32
32
  const result = await run('cf', args, {
@@ -86,7 +86,7 @@ export async function getCloudFoundryTarget({ env = process.env, execFileImpl =
86
86
  if (!spaceGuid || /\s/.test(spaceGuid)) {
87
87
  return { available: false, reason: 'Cloud Foundry CLI is not authenticated to a targeted space; CF destinations were skipped.' };
88
88
  }
89
- return { available: true, spaceGuid };
89
+ return { available: true, spaceGuid, spaceName, orgName: /^\s*org:\s*(.*?)\s*$/im.exec(target)?.[1] || '' };
90
90
  }
91
91
 
92
92
  function missingServiceKey(error) {
@@ -159,7 +159,7 @@ export async function findOrphanedCloudFoundryServiceKeys({ env = process.env, s
159
159
  return orphans;
160
160
  }
161
161
 
162
- async function cfJson(path, options) {
162
+ export async function cfJson(path, options) {
163
163
  const value = await runCf(['curl', path], options);
164
164
  try {
165
165
  return JSON.parse(value);
@@ -177,7 +177,7 @@ function apiPath(href) {
177
177
  }
178
178
  }
179
179
 
180
- async function serviceInstances(spaceGuid, options) {
180
+ export async function serviceInstances(spaceGuid, options) {
181
181
  const instances = [];
182
182
  let path = `/v3/service_instances?space_guids=${encodeURIComponent(spaceGuid)}&per_page=500`;
183
183
  const visited = new Set();
@@ -398,7 +398,7 @@ async function readServiceKey(instance, kind, spaceGuid, managedKeys, createdKey
398
398
  return { keyName, credentials };
399
399
  }
400
400
 
401
- async function serviceKeyCredentials(instanceName, keyName, kind, options) {
401
+ export async function serviceKeyCredentials(instanceName, keyName, kind, options) {
402
402
  const raw = await runCf(['service-key', '--json', instanceName, keyName], options);
403
403
  let parsed;
404
404
  try {
@@ -0,0 +1,74 @@
1
+ // Integration Suite credentials from the logged-in Cloud Foundry space: find the Process Integration
2
+ // Runtime (plan api) and API Management (plan apiportal-apiaccess) instances, reuse or create one
3
+ // named service key per instance, and optionally create a dedicated instance with the roles the tools need.
4
+ import { execFile as nodeExecFile } from 'node:child_process';
5
+ import { cfJson, runCf, serviceInstances, serviceKeyCredentials } from './cf-destination.mjs';
6
+
7
+ // Not the toolkit's destination-key prefix: the destination orphan sweep must not offer to delete these keys.
8
+ export const INTEGRATION_KEY_NAME = 'sap-ai-dev-integration';
9
+
10
+ export const INTEGRATION_CF_SERVICES = Object.freeze({
11
+ CPI: Object.freeze({
12
+ offering: 'it-rt',
13
+ plan: 'api',
14
+ label: 'Process Integration Runtime',
15
+ dedicatedInstance: 'sap-ai-dev-integration-cpi',
16
+ // Role templates of the Cloud Integration API plan covering design, deployment, monitoring,
17
+ // security material and Partner Directory work done by the cpi_* tools.
18
+ parameters: Object.freeze({
19
+ 'grant-types': ['client_credentials'],
20
+ 'redirect-uris': [],
21
+ roles: ['AuthGroup_Administrator', 'AuthGroup_BusinessExpert', 'AuthGroup_IntegrationDeveloper', 'AuthGroup_TenantPartnerDirectoryConfigurator', 'AccessAllAccessPoliciesArtifacts']
22
+ })
23
+ }),
24
+ APIM: Object.freeze({
25
+ offering: 'apimanagement-apiportal',
26
+ plan: 'apiportal-apiaccess',
27
+ label: 'API Management, API portal',
28
+ dedicatedInstance: 'sap-ai-dev-integration-apim',
29
+ parameters: Object.freeze({ role: 'APIPortal.Administrator' })
30
+ })
31
+ });
32
+
33
+ /** Integration Suite instances in the space, by side: { CPI: [{ guid, name }], APIM: [...] }. */
34
+ export async function findIntegrationInstances({ env = process.env, spaceGuid, execFileImpl = nodeExecFile } = {}) {
35
+ const options = { env, execFileImpl };
36
+ const plans = new Map();
37
+ const found = { CPI: [], APIM: [] };
38
+ for (const instance of await serviceInstances(spaceGuid, options)) {
39
+ const planGuid = instance?.relationships?.service_plan?.data?.guid;
40
+ if (!planGuid || !instance?.name) continue;
41
+ if (!plans.has(planGuid)) {
42
+ try {
43
+ const plan = await cfJson(`/v3/service_plans/${encodeURIComponent(planGuid)}?include=service_offering`, options);
44
+ plans.set(planGuid, { plan: plan?.name, offering: plan?.included?.service_offerings?.[0]?.name });
45
+ } catch {
46
+ plans.set(planGuid, {});
47
+ }
48
+ }
49
+ const { plan, offering } = plans.get(planGuid);
50
+ for (const [side, service] of Object.entries(INTEGRATION_CF_SERVICES)) {
51
+ if (offering === service.offering && plan === service.plan) found[side].push({ guid: String(instance.guid), name: String(instance.name) });
52
+ }
53
+ }
54
+ return found;
55
+ }
56
+
57
+ /** Credentials of the toolkit's service key on an instance; the key is created when it does not exist yet. */
58
+ export async function integrationServiceKey(instanceName, { env = process.env, execFileImpl = nodeExecFile } = {}) {
59
+ const options = { env, execFileImpl };
60
+ try {
61
+ return { keyName: INTEGRATION_KEY_NAME, created: false, credentials: await serviceKeyCredentials(instanceName, INTEGRATION_KEY_NAME, 'Integration Suite', options) };
62
+ } catch {
63
+ // No readable key of that name yet.
64
+ }
65
+ await runCf(['create-service-key', instanceName, INTEGRATION_KEY_NAME, '--wait'], options);
66
+ return { keyName: INTEGRATION_KEY_NAME, created: true, credentials: await serviceKeyCredentials(instanceName, INTEGRATION_KEY_NAME, 'Integration Suite', options) };
67
+ }
68
+
69
+ /** Create the dedicated instance for a side with the roles the tools need; returns { guid?, name }. */
70
+ export async function createIntegrationInstance(side, { env = process.env, execFileImpl = nodeExecFile } = {}) {
71
+ const service = INTEGRATION_CF_SERVICES[side];
72
+ await runCf(['create-service', service.offering, service.plan, service.dedicatedInstance, '-c', JSON.stringify(service.parameters), '--wait'], { env, execFileImpl });
73
+ return { name: service.dedicatedInstance };
74
+ }
@@ -80,6 +80,22 @@ function setCookiePairs(headers) {
80
80
  return values.map(value => value.split(';')[0].trim()).filter(Boolean);
81
81
  }
82
82
 
83
+ // undici flags EnvHttpProxyAgent as experimental with a process warning that lands in the middle of the
84
+ // setup wizard output; only that warning is dropped, and only while the agent is constructed.
85
+ function envProxyAgent(options) {
86
+ const emitWarning = process.emitWarning;
87
+ process.emitWarning = (warning, ...rest) => {
88
+ const code = typeof rest[0] === 'object' ? rest[0]?.code : rest[1];
89
+ if (code === 'UNDICI-EHPA') return;
90
+ emitWarning.call(process, warning, ...rest);
91
+ };
92
+ try {
93
+ return new EnvHttpProxyAgent(options);
94
+ } finally {
95
+ process.emitWarning = emitWarning;
96
+ }
97
+ }
98
+
83
99
  /** Create a tenant-scoped client using OAuth client credentials. */
84
100
  export function createServiceClient({
85
101
  label,
@@ -97,7 +113,7 @@ export function createServiceClient({
97
113
  const httpProxy = env.HTTP_PROXY || env.http_proxy;
98
114
  const httpsProxy = env.HTTPS_PROXY || env.https_proxy;
99
115
  const dispatcher = httpProxy || httpsProxy
100
- ? new EnvHttpProxyAgent({ httpProxy, httpsProxy, noProxy: env.NO_PROXY || env.no_proxy })
116
+ ? envProxyAgent({ httpProxy, httpsProxy, noProxy: env.NO_PROXY || env.no_proxy })
101
117
  : undefined;
102
118
  const transportOptions = dispatcher ? { dispatcher } : {};
103
119
  let cachedToken = null;
@@ -109,7 +109,7 @@ export async function verifyIntegrationClients({ cpi, apim } = {}) {
109
109
  export function verificationLines(label, host, side) {
110
110
  if (!side?.configured) return [];
111
111
  if (!side.signed_in) return [`✗ ${label} ${host}: sign-in failed — ${side.error}`];
112
- const lines = [`${side.ok ? '✓' : '✗'} ${label} ${host}: signed in`];
112
+ const lines = [side.ok ? `✓ ${label} ${host}: signed in` : `✗ ${label} ${host}: signed in, but ${side.checks[0]?.area ?? 'the core area'} is not readable with this key`];
113
113
  for (const check of side.checks) {
114
114
  const mark = check.result === 'ok' ? '✓' : (check.result === 'unavailable' ? '–' : '✗');
115
115
  lines.push(` ${mark} ${check.area}${check.result === 'ok' ? '' : ` — ${check.detail}`}`);
package/src/setup.mjs CHANGED
@@ -10,6 +10,7 @@ import { discoverCloudFoundryDestinations, getCloudFoundryTarget, deleteManagedC
10
10
  import { SAP_DEVELOPMENT_MCP_SERVERS, collectCloudFoundryKeyReferencesFromAllEntries, collectManagedCloudFoundryKeyReferences, installMcpConfig, readMcpConfig, resolveMcpConfigPath } from './mcp-config.mjs';
11
11
  import { parseIntegrationServiceKey, resolveIntegrationConfig, resolveIntegrationSideFromEnvironment } from './integration-config.mjs';
12
12
  import { readStoredIntegrationKeys, storeIntegrationKey } from './integration-credentials.mjs';
13
+ import { INTEGRATION_CF_SERVICES, createIntegrationInstance, findIntegrationInstances, integrationServiceKey } from './cf-integration.mjs';
13
14
  import { createIntegrationClients, verificationLines, verifyIntegrationClients } from './integration-verify.mjs';
14
15
  import { discoverCertificateDnsNames } from './tls-adt-proxy.mjs';
15
16
  import { findRemoteControllableSsoBrowsers } from './sso-browsers.mjs';
@@ -331,23 +332,148 @@ async function printIntegrationVerification(config, { env, output, verifyIntegra
331
332
  if (report.ok) {
332
333
  print(output, formatStatus(`Integration Suite credentials verified${missing ? `; ${missing} area${missing === 1 ? ' needs' : 's need'} an extra role on the service key` : ''}.`, missing ? 'warning' : 'success', output, 'Integration Suite'));
333
334
  } else {
334
- print(output, formatStatus('Integration Suite verification failed; fix or replace the service key before using the tools.', 'warning', output, 'Integration Suite'));
335
+ const signedIn = [report.cpi, report.apim].some(side => side?.signed_in);
336
+ print(output, formatStatus(signedIn
337
+ ? 'The key signs in but lacks the roles for the core area; use an instance with the needed roles (the Cloud Foundry option can create one) or replace the key.'
338
+ : 'Integration Suite sign-in failed; fix or replace the service key before using the tools.', 'warning', output, 'Integration Suite'));
335
339
  }
336
340
  return report;
337
341
  }
338
342
 
339
- // Verify the detected credentials online and let the user paste CPI/APIM service keys. A pasted key is checked
340
- // online first and saved to the user-only store (never to mcp.json); it is used when the environment has no key.
341
- async function configureIntegrationCredentials({ env, input, output, verifyIntegration }) {
343
+ const defaultIntegrationCloudFoundry = Object.freeze({
344
+ getTarget: getCloudFoundryTarget,
345
+ findInstances: findIntegrationInstances,
346
+ serviceKey: integrationServiceKey,
347
+ createInstance: createIntegrationInstance
348
+ });
349
+
350
+ const SIDE_LABELS = Object.freeze({ CPI: 'Cloud Integration (CPI)', APIM: 'API Management' });
351
+
352
+ function cfFailure(error) {
353
+ const detail = String(error?.stderr || error?.message || '').replace(/\s+/g, ' ').trim();
354
+ return detail ? detail.slice(0, 300) : 'Cloud Foundry CLI command failed';
355
+ }
356
+
357
+ // Verify one side's key online, then save it user-only. A key that cannot sign in is saved only when confirmed.
358
+ async function acceptIntegrationKey(side, config, { env, input, output, verifyIntegration }) {
359
+ const choice = side.toLowerCase();
360
+ const report = await printIntegrationVerification({ [choice]: config }, { env, output, verifyIntegration });
361
+ if (!report[choice].signed_in && !await confirmPrompt('Save this key anyway? (y/n, Enter=no)', { input, output, defaultYes: false })) return { saved: false, report };
362
+ const path = await storeIntegrationKey(side, config, env);
363
+ print(output, formatStatus(`Saved the ${side} key to ${path} (readable only by you; not written to mcp.json).`, 'success', output, 'Integration Suite'));
364
+ let fromEnvironment = null;
365
+ try {
366
+ fromEnvironment = resolveIntegrationSideFromEnvironment(env, side);
367
+ } catch {
368
+ // A broken environment value is reported by the status lines.
369
+ }
370
+ if (fromEnvironment) print(output, formatStatus(`${side} credentials from ${fromEnvironment.source} in this environment take precedence over the saved key.`, 'warning', output, 'Integration Suite'));
371
+ return { saved: true, report };
372
+ }
373
+
374
+ function lacksRoles(side) {
375
+ return Boolean(side?.signed_in) && (!side.ok || side.checks.some(check => check.result === 'forbidden'));
376
+ }
377
+
378
+ async function cloudFoundryStep(message, operation, { env, output }) {
379
+ const stop = startProgress(message, { output, env, label: 'Cloud Foundry' });
380
+ try {
381
+ return await operation();
382
+ } finally {
383
+ stop();
384
+ }
385
+ }
386
+
387
+ // Key from an instance: reuse or create the toolkit's service key, then verify and save it.
388
+ async function useCloudFoundryInstance(side, instanceName, context) {
389
+ const { env, output, cloudFoundry } = context;
390
+ const key = await cloudFoundryStep(`Reading service key ${instanceName}/sap-ai-dev-integration`, () => cloudFoundry.serviceKey(instanceName, { env }), context);
391
+ print(output, formatStatus(`${key.created ? 'Created' : 'Reusing'} service key ${key.keyName} on ${instanceName}.`, 'info', output, 'Cloud Foundry'));
392
+ return acceptIntegrationKey(side, parseIntegrationServiceKey(side, key.credentials), context);
393
+ }
394
+
395
+ async function createDedicatedInstance(side, context) {
396
+ const { env, output, cloudFoundry } = context;
397
+ const service = INTEGRATION_CF_SERVICES[side];
398
+ try {
399
+ await cloudFoundryStep(`Creating ${service.offering}/${service.plan} instance ${service.dedicatedInstance}`, () => cloudFoundry.createInstance(side, { env }), context);
400
+ } catch (error) {
401
+ print(output, formatStatus(`Could not create ${service.dedicatedInstance}: ${cfFailure(error)}`, 'warning', output, 'Cloud Foundry'));
402
+ return null;
403
+ }
404
+ print(output, formatStatus(`Created service instance ${service.dedicatedInstance}.`, 'success', output, 'Cloud Foundry'));
405
+ return useCloudFoundryInstance(side, service.dedicatedInstance, context);
406
+ }
407
+
408
+ // Use the logged-in CF space: per side pick the matching instance (or offer to create a dedicated one with the
409
+ // roles the tools need), reuse or create the toolkit's service key, verify it online and save it.
410
+ async function useCloudFoundrySpace(target, context) {
411
+ const { env, input, output, cloudFoundry } = context;
412
+ let instances;
413
+ try {
414
+ instances = await cloudFoundryStep('Looking for Integration Suite service instances in the CF space', () => cloudFoundry.findInstances({ env, spaceGuid: target.spaceGuid }), context);
415
+ } catch (error) {
416
+ print(output, formatStatus(`Could not list service instances: ${cfFailure(error)}`, 'warning', output, 'Cloud Foundry'));
417
+ return;
418
+ }
419
+ for (const side of ['CPI', 'APIM']) {
420
+ const service = INTEGRATION_CF_SERVICES[side];
421
+ const candidates = instances[side] || [];
422
+ print(output, '');
423
+ if (!candidates.length) {
424
+ print(output, formatStatus(`No ${service.label} instance with plan ${service.plan} (${service.offering}) in this space.`, 'info', output, SIDE_LABELS[side]));
425
+ if (await confirmPrompt(`Create instance ${service.dedicatedInstance} (${service.offering}, plan ${service.plan}) with the roles the tools need? (y/n, Enter=no)`, { input, output, defaultYes: false })) {
426
+ await createDedicatedInstance(side, context);
427
+ }
428
+ continue;
429
+ }
430
+ const dedicated = candidates.find(candidate => candidate.name === service.dedicatedInstance);
431
+ const instanceName = candidates.length === 1 ? candidates[0].name : await selectPrompt({
432
+ message: colorText(`☁️ ${SIDE_LABELS[side]}: choose the ${service.offering}/${service.plan} instance to use`, 'cyan', output),
433
+ defaultValue: (dedicated || candidates[0]).name,
434
+ choices: [...candidates.map(candidate => ({ name: candidate.name, value: candidate.name })), { name: 'Skip', value: '', shortcut: 's' }]
435
+ }, { input, output });
436
+ if (!instanceName) continue;
437
+ let result;
438
+ try {
439
+ result = await useCloudFoundryInstance(side, instanceName, context);
440
+ } catch (error) {
441
+ print(output, formatStatus(`Could not use ${instanceName}: ${cfFailure(error)}`, 'warning', output, SIDE_LABELS[side]));
442
+ continue;
443
+ }
444
+ const sideReport = result.report[side.toLowerCase()];
445
+ if (instanceName !== service.dedicatedInstance && !dedicated && lacksRoles(sideReport)) {
446
+ print(output, ` The roles of ${instanceName} are fixed on the instance; a new key cannot add them.`);
447
+ print(output, ` A dedicated instance gets: ${service.parameters.roles ? service.parameters.roles.join(', ') : service.parameters.role}.`);
448
+ if (await confirmPrompt(`Create instance ${service.dedicatedInstance} with these roles and use it instead? (y/n, Enter=no)`, { input, output, defaultYes: false })) {
449
+ await createDedicatedInstance(side, context);
450
+ }
451
+ }
452
+ }
453
+ }
454
+
455
+ // Verify the detected credentials online and let the user take CPI/APIM keys from the logged-in CF space or
456
+ // paste them. A key is checked online first and saved to the user-only store (never to mcp.json); it is used
457
+ // when the environment has no key.
458
+ async function configureIntegrationCredentials({ env, input, output, verifyIntegration, cloudFoundry = defaultIntegrationCloudFoundry }) {
342
459
  let status = integrationSuiteCredentialStatus(env);
343
460
  if (status.found) await printIntegrationVerification(status.config, { env, output, verifyIntegration });
461
+ let target = { available: false };
462
+ try {
463
+ target = await cloudFoundryStep('Checking the Cloud Foundry login', () => cloudFoundry.getTarget({ env }), { env, output });
464
+ } catch {
465
+ // No CF CLI or no login: the CF option is simply not offered.
466
+ }
467
+ const context = { env, input, output, verifyIntegration, cloudFoundry };
344
468
  for (;;) {
345
469
  print(output, '');
470
+ const space = target.available ? [target.orgName, target.spaceName].filter(Boolean).join(' / ') || target.spaceGuid : '';
346
471
  const choice = await selectPrompt({
347
472
  message: colorText('🔑 Integration Suite credentials', 'cyan', output),
348
- defaultValue: status.found ? 'done' : 'cpi',
473
+ defaultValue: status.found ? 'done' : (target.available ? 'cf' : 'cpi'),
349
474
  choices: [
350
475
  { name: status.found ? 'Continue' : 'Continue without credentials (the server will not start until a key is configured)', value: 'done', shortcut: 'c' },
476
+ ...(target.available ? [{ name: `Use the logged-in Cloud Foundry space (${space}): find the instances, create or reuse service keys, verify`, value: 'cf', shortcut: 'f' }] : []),
351
477
  { name: 'Paste a Cloud Integration (CPI) service key — Process Integration Runtime, plan api', value: 'cpi', shortcut: 'p' },
352
478
  { name: 'Paste an API Management service key — API Management, plan apiportal-apiaccess', value: 'apim', shortcut: 'a' },
353
479
  ...(status.found ? [{ name: 'Check the configured credentials online again', value: 'verify', shortcut: 'v' }] : [])
@@ -358,37 +484,30 @@ async function configureIntegrationCredentials({ env, input, output, verifyInteg
358
484
  await printIntegrationVerification(status.config, { env, output, verifyIntegration });
359
485
  continue;
360
486
  }
361
- const side = choice.toUpperCase();
362
- const pasted = await textPrompt({
363
- message: `Paste the ${side} service key JSON or the path to its .json file`,
364
- secret: true,
365
- validate: value => {
366
- try {
367
- pastedServiceKey(side, value, env);
368
- return true;
369
- } catch (error) {
370
- return error.message;
487
+ if (choice === 'cf') {
488
+ await useCloudFoundrySpace(target, context);
489
+ } else {
490
+ const side = choice.toUpperCase();
491
+ const pasted = await textPrompt({
492
+ message: `Paste the ${side} service key JSON or the path to its .json file`,
493
+ secret: true,
494
+ validate: value => {
495
+ try {
496
+ pastedServiceKey(side, value, env);
497
+ return true;
498
+ } catch (error) {
499
+ return error.message;
500
+ }
371
501
  }
372
- }
373
- }, { input, output });
374
- const config = pastedServiceKey(side, pasted, env);
375
- const report = await printIntegrationVerification({ [choice]: config }, { env, output, verifyIntegration });
376
- if (!report[choice].signed_in && !await confirmPrompt('Save this key anyway? (y/n, Enter=no)', { input, output, defaultYes: false })) continue;
377
- const path = await storeIntegrationKey(side, config, env);
378
- print(output, formatStatus(`Saved the ${side} key to ${path} (readable only by you; not written to mcp.json).`, 'success', output, 'Integration Suite'));
379
- let fromEnvironment = null;
380
- try {
381
- fromEnvironment = resolveIntegrationSideFromEnvironment(env, side);
382
- } catch {
383
- // A broken environment value is reported by the status lines below.
502
+ }, { input, output });
503
+ await acceptIntegrationKey(side, pastedServiceKey(side, pasted, env), context);
384
504
  }
385
- if (fromEnvironment) print(output, formatStatus(`${side} credentials from ${fromEnvironment.source} in this environment take precedence over the saved key.`, 'warning', output, 'Integration Suite'));
386
505
  status = integrationSuiteCredentialStatus(env);
387
506
  for (const line of status.lines) print(output, ` ${line}`);
388
507
  }
389
508
  }
390
509
 
391
- async function promptIntegrationSuite({ env, alreadyConfigured, input, output, verifyIntegration = defaultVerifyIntegration, ask = true }) {
510
+ async function promptIntegrationSuite({ env, alreadyConfigured, input, output, verifyIntegration = defaultVerifyIntegration, integrationCloudFoundry, ask = true }) {
392
511
  print(output, '');
393
512
  print(output, formatStatus('SAP Integration Suite tools (Cloud Integration + API Management).', 'step', output, 'Integration Suite'));
394
513
  print(output, ' Adds the sap-ai-integration MCP server: iFlows, value mappings, security material and monitoring in CPI; API providers, proxies, policies, products and key value maps in APIM.');
@@ -405,7 +524,7 @@ async function promptIntegrationSuite({ env, alreadyConfigured, input, output, v
405
524
  const defaultYes = status.found;
406
525
  if (!await confirmPrompt(`Add the SAP Integration Suite tools? (y/n, Enter=${defaultYes ? 'yes' : 'no'})`, { input, output, defaultYes })) return false;
407
526
  }
408
- await configureIntegrationCredentials({ env, input, output, verifyIntegration });
527
+ await configureIntegrationCredentials({ env, input, output, verifyIntegration, ...(integrationCloudFoundry ? { cloudFoundry: integrationCloudFoundry } : {}) });
409
528
  return true;
410
529
  }
411
530
 
@@ -415,7 +534,8 @@ export async function runIntegrationSuiteSetup({
415
534
  input = stdin,
416
535
  output = stdout,
417
536
  install = installMcpConfig,
418
- verifyIntegration = defaultVerifyIntegration
537
+ verifyIntegration = defaultVerifyIntegration,
538
+ integrationCloudFoundry
419
539
  } = {}) {
420
540
  if (!input.isTTY || !output.isTTY) {
421
541
  print(output, formatStatus('Interactive setup needs a terminal. Rerun sap-ai-dev --setup-integration from a terminal.', 'warning', output, 'SAP AI Dev Toolkit'));
@@ -427,7 +547,7 @@ export async function runIntegrationSuiteSetup({
427
547
  } catch {
428
548
  // A missing or unreadable config is created or healed by install.
429
549
  }
430
- await promptIntegrationSuite({ env, alreadyConfigured, input, output, verifyIntegration, ask: false });
550
+ await promptIntegrationSuite({ env, alreadyConfigured, input, output, verifyIntegration, integrationCloudFoundry, ask: false });
431
551
  const result = await install([], { env, sapDevelopmentServers: ['integration-suite'], additive: true });
432
552
  print(output, '');
433
553
  print(output, formatStatus(`Configured the Integration Suite MCP server${result?.path ? ` in ${result.path}` : ''}.`, 'success', output, 'Integration Suite'));
@@ -771,7 +891,8 @@ export async function runSetup({
771
891
  includeSapDevelopmentToolsPrompt = false,
772
892
  detectAdtUrl,
773
893
  discoverSsoBrowsers = findRemoteControllableSsoBrowsers,
774
- verifyIntegration = defaultVerifyIntegration
894
+ verifyIntegration = defaultVerifyIntegration,
895
+ integrationCloudFoundry
775
896
  } = {}) {
776
897
  const isBas = Boolean(env.H2O_URL);
777
898
  if (!isBas && (!input.isTTY || !output.isTTY)) {
@@ -943,7 +1064,7 @@ export async function runSetup({
943
1064
 
944
1065
  let sapDevelopmentServers = [];
945
1066
  if (integrationOnly) {
946
- await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, ask: false });
1067
+ await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, integrationCloudFoundry, ask: false });
947
1068
  sapDevelopmentServers = ['integration-suite'];
948
1069
  } else if (includeSapDevelopmentToolsPrompt) {
949
1070
  print(output, '');
@@ -963,9 +1084,9 @@ export async function runSetup({
963
1084
  shortcuts: { all: 'a' }
964
1085
  }, { input, output });
965
1086
  if (sapDevelopmentServers.includes('integration-suite')) {
966
- await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, ask: false });
1087
+ await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, integrationCloudFoundry, ask: false });
967
1088
  }
968
- } else if (await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration })) {
1089
+ } else if (await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, integrationCloudFoundry })) {
969
1090
  // Without --tools, Integration Suite still gets its own yes/no step instead of being hidden.
970
1091
  sapDevelopmentServers = ['integration-suite'];
971
1092
  }