sap-ai-dev-toolkit 0.9.9921 → 0.9.9923
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/.github/agents/sap-btp-infrastructure-admin.agent.md +23 -0
- package/.github/agents/sap-bw-datasphere-migration-specialist.agent.md +22 -0
- package/.github/agents/sap-datasphere-developer.agent.md +22 -0
- package/.github/agents/sap-devops-observability-specialist.agent.md +54 -0
- package/.github/agents/sap-process-automation-developer.agent.md +50 -0
- package/.github/skills/btp-platform-administration/SKILL.md +53 -0
- package/.github/skills/bw-datasphere-migration/SKILL.md +66 -0
- package/.github/skills/cloud-alm-operations/SKILL.md +135 -0
- package/.github/skills/datasphere-administration/SKILL.md +40 -0
- package/.github/skills/datasphere-development/SKILL.md +55 -0
- package/.github/skills/process-automation-development/SKILL.md +113 -0
- package/NOTICE +2 -1
- package/README.md +152 -8
- package/dist/checksums.txt +5 -5
- package/dist/vsp-darwin-arm64 +0 -0
- package/dist/vsp-darwin-x64 +0 -0
- package/dist/vsp-linux-arm64 +0 -0
- package/dist/vsp-linux-x64 +0 -0
- package/dist/vsp-win32-x64.exe +0 -0
- package/inventory.md +60 -0
- package/package.json +8 -2
- package/patches/vsp-namespaced-class-include-edit.patch +134 -0
- package/scripts/build-vsp.mjs +3 -1
- package/src/btp-cli.mjs +199 -0
- package/src/btp-server.mjs +125 -0
- package/src/btp-tools.mjs +776 -0
- package/src/cloud-alm-client.mjs +44 -0
- package/src/cloud-alm-config.mjs +92 -0
- package/src/cloud-alm-credentials.mjs +29 -0
- package/src/cloud-alm-data.mjs +129 -0
- package/src/cloud-alm-server.mjs +115 -0
- package/src/cloud-alm-tools.mjs +682 -0
- package/src/datasphere-auth.mjs +279 -0
- package/src/datasphere-cli.mjs +220 -0
- package/src/datasphere-command.mjs +236 -0
- package/src/datasphere-config.mjs +154 -0
- package/src/datasphere-credentials.mjs +37 -0
- package/src/datasphere-csn.mjs +73 -0
- package/src/datasphere-discovery.mjs +151 -0
- package/src/datasphere-server.mjs +249 -0
- package/src/datasphere-tools.mjs +809 -0
- package/src/hana-database.mjs +3 -3
- package/src/hana-inspector.mjs +2 -2
- package/src/harnesses.mjs +31 -1
- package/src/install-user-assets.mjs +1 -1
- package/src/integration-common.mjs +7 -7
- package/src/integration-config.mjs +5 -0
- package/src/integration-credentials.mjs +25 -16
- package/src/integration-http.mjs +24 -7
- package/src/launcher.mjs +56 -4
- package/src/mcp-config.mjs +37 -1
- package/src/process-automation-automation-tools.mjs +125 -0
- package/src/process-automation-client.mjs +36 -0
- package/src/process-automation-common.mjs +69 -0
- package/src/process-automation-config.mjs +142 -0
- package/src/process-automation-credentials.mjs +37 -0
- package/src/process-automation-inbox-tools.mjs +246 -0
- package/src/process-automation-server.mjs +139 -0
- package/src/process-automation-tools.mjs +727 -0
- package/src/process-automation-visibility-tools.mjs +182 -0
- package/src/setup.mjs +623 -11
- package/tools.md +138 -0
package/src/setup.mjs
CHANGED
|
@@ -15,6 +15,19 @@ import { createIntegrationClients, verificationLines, verifyIntegrationClients }
|
|
|
15
15
|
import { OPTIONAL_COMPONENT_KEYS, OPTIONAL_INPUTS, parseOptionalConfig, resolveOptionalConfig } from './integration-optional-config.mjs';
|
|
16
16
|
import { discoverCertificateDnsNames } from './tls-adt-proxy.mjs';
|
|
17
17
|
import { findRemoteControllableSsoBrowsers } from './sso-browsers.mjs';
|
|
18
|
+
import { parseDatasphereCredentials, resolveDatasphereConfig } from './datasphere-config.mjs';
|
|
19
|
+
import { datasphereCredentialsPath, readStoredDatasphereCredentials, storeDatasphereCredentials } from './datasphere-credentials.mjs';
|
|
20
|
+
import { authorizationCodeLogin, createDatasphereTokenProvider, openInBrowser } from './datasphere-auth.mjs';
|
|
21
|
+
import { PROCESS_AUTOMATION_OAUTH, parseProcessAutomationCredentials, parseProcessAutomationServiceKey, resolveProcessAutomationConfig } from './process-automation-config.mjs';
|
|
22
|
+
import { readStoredProcessAutomationCredentials, storeProcessAutomationCredentials } from './process-automation-credentials.mjs';
|
|
23
|
+
import { createProcessAutomationRestClient } from './process-automation-client.mjs';
|
|
24
|
+
import { createBtpCli, locateBtpCli, loginBtpCli, readBtpSettings, storeBtpSettings } from './btp-cli.mjs';
|
|
25
|
+
import { parseCloudAlmCredentials, parseCloudAlmServiceKey, resolveCloudAlmConfig } from './cloud-alm-config.mjs';
|
|
26
|
+
import { readStoredCloudAlmCredentials, storeCloudAlmCredentials } from './cloud-alm-credentials.mjs';
|
|
27
|
+
import { createCloudAlmRestClient, verifyCloudAlmAccess } from './cloud-alm-client.mjs';
|
|
28
|
+
import { createStaticServiceClient } from './integration-http.mjs';
|
|
29
|
+
import { connectHana } from './hana-database.mjs';
|
|
30
|
+
import { diagnosticText } from './redact.mjs';
|
|
18
31
|
|
|
19
32
|
const SETUP_COMMAND = 'sap-ai-dev --setup';
|
|
20
33
|
|
|
@@ -314,18 +327,21 @@ function defaultVerifyIntegration(config, { env }) {
|
|
|
314
327
|
return verifyIntegrationClients(createIntegrationClients(config, { env }));
|
|
315
328
|
}
|
|
316
329
|
|
|
317
|
-
/** Read
|
|
318
|
-
function
|
|
330
|
+
/** Read pasted JSON text (line breaks from a multi-line paste are dropped) or the contents of a .json file path. */
|
|
331
|
+
function readPastedJson(value, env) {
|
|
319
332
|
const trimmed = String(value || '').trim();
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
throw new Error('Paste the service key JSON (starting with {) or the path to a readable .json file.');
|
|
327
|
-
}
|
|
333
|
+
if (trimmed.startsWith('{')) return trimmed;
|
|
334
|
+
const path = trimmed.replace(/^~(?=$|[\\/])/, env.HOME || env.USERPROFILE || '~');
|
|
335
|
+
try {
|
|
336
|
+
return readFileSync(path, 'utf8');
|
|
337
|
+
} catch {
|
|
338
|
+
throw new Error('Paste the service key JSON (starting with {) or the path to a readable .json file.');
|
|
328
339
|
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Read a pasted service key: JSON text or a path to a .json file. */
|
|
343
|
+
function pastedServiceKey(side, value, env) {
|
|
344
|
+
const text = readPastedJson(value, env);
|
|
329
345
|
return OPTIONAL_COMPONENT_KEYS.includes(side.toLowerCase())
|
|
330
346
|
? parseOptionalConfig(side.toLowerCase(), text, { source: 'setup', allowRawCtms: true })
|
|
331
347
|
: parseIntegrationServiceKey(side, text);
|
|
@@ -608,6 +624,586 @@ export async function runIntegrationSuiteSetup({
|
|
|
608
624
|
return { selected: [], ...(result || {}) };
|
|
609
625
|
}
|
|
610
626
|
|
|
627
|
+
export function datasphereCredentialStatus(env = process.env) {
|
|
628
|
+
try {
|
|
629
|
+
const config = resolveDatasphereConfig(env, { stored: readStoredDatasphereCredentials(env) });
|
|
630
|
+
const lines = [`Datasphere tenant ${new URL(config.url).host} (${config.auth}, source: ${config.source})`];
|
|
631
|
+
if (config.sql) lines.push(`SQL user ${config.sql.user}@${config.sql.host}`);
|
|
632
|
+
return { found: true, source: config.source, auth: config.auth, config, lines };
|
|
633
|
+
} catch (error) {
|
|
634
|
+
return { found: false, source: null, auth: null, config: null, lines: [String(error?.message || '')] };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function datasphereRestClient(config, { env, accessToken } = {}) {
|
|
639
|
+
const shared = {
|
|
640
|
+
label: 'DATASPHERE',
|
|
641
|
+
url: config.url,
|
|
642
|
+
csrfPath: '/api/v1/datasphere/consumption/catalog/spaces',
|
|
643
|
+
env
|
|
644
|
+
};
|
|
645
|
+
if (accessToken) {
|
|
646
|
+
return createStaticServiceClient({ ...shared, authorization: async () => `Bearer ${accessToken}` });
|
|
647
|
+
}
|
|
648
|
+
const provider = createDatasphereTokenProvider({ config, env });
|
|
649
|
+
return createStaticServiceClient({ ...shared, authorization: async () => `Bearer ${(await provider.token()).accessToken}` });
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function defaultVerifyDatasphere(config, { env, accessToken } = {}) {
|
|
653
|
+
try {
|
|
654
|
+
const payload = await datasphereRestClient(config, { env, accessToken }).request('GET', '/api/v1/datasphere/consumption/catalog/spaces');
|
|
655
|
+
return { ok: true, message: `signed in; ${(payload?.value ?? []).length} spaces visible` };
|
|
656
|
+
} catch (error) {
|
|
657
|
+
return { ok: false, message: diagnosticText(error) };
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Prompt for a Datasphere OAuth client, verify it online, optionally add SQL access, and store it. */
|
|
662
|
+
export async function promptDatasphere({
|
|
663
|
+
env = process.env,
|
|
664
|
+
input = stdin,
|
|
665
|
+
output = stdout,
|
|
666
|
+
verifyDatasphere = defaultVerifyDatasphere,
|
|
667
|
+
login = authorizationCodeLogin
|
|
668
|
+
} = {}) {
|
|
669
|
+
print(output, '');
|
|
670
|
+
print(output, formatStatus('SAP Datasphere tools', 'step', output, 'Datasphere'));
|
|
671
|
+
print(output, ' Connects your AI agent to SAP Datasphere through an OAuth client from System > App Integration. Credentials are checked online and stored only for you, never in mcp.json.');
|
|
672
|
+
const status = datasphereCredentialStatus(env);
|
|
673
|
+
for (const line of status.lines) print(output, ` ${line}`);
|
|
674
|
+
if (status.found) {
|
|
675
|
+
if (status.source === 'environment') return true;
|
|
676
|
+
if (!await confirmPrompt('Replace the stored Datasphere sign-in? (y/n, Enter=no)', { input, output, defaultYes: false })) return true;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const auth = await selectPrompt({
|
|
680
|
+
message: 'How should the toolkit sign in to SAP Datasphere?',
|
|
681
|
+
defaultValue: 'technical',
|
|
682
|
+
choices: [
|
|
683
|
+
{ name: 'Technical user (Technical User OAuth client): objects, tasks, Data Marketplace, connections, certificates, consumption data', value: 'technical', shortcut: 't' },
|
|
684
|
+
{ name: 'Your user (Interactive Usage OAuth client, redirect URI http://localhost:8080): everything, including spaces, users, roles, database users and workload', value: 'interactive', shortcut: 'y' }
|
|
685
|
+
]
|
|
686
|
+
}, { input, output });
|
|
687
|
+
|
|
688
|
+
const FIELD_LABELS = { url: 'DATASPHERE_URL', tokenUrl: 'DATASPHERE_TOKEN_URL', authorizationUrl: 'DATASPHERE_AUTHORIZATION_URL' };
|
|
689
|
+
const validateUrl = field => value => {
|
|
690
|
+
try {
|
|
691
|
+
parseDatasphereCredentials({
|
|
692
|
+
auth,
|
|
693
|
+
url: field === 'url' ? value : 'https://placeholder.invalid',
|
|
694
|
+
tokenUrl: field === 'tokenUrl' ? value : 'https://placeholder.invalid/oauth/token',
|
|
695
|
+
clientId: 'placeholder',
|
|
696
|
+
clientSecret: 'placeholder',
|
|
697
|
+
...(auth === 'interactive'
|
|
698
|
+
? { authorizationUrl: field === 'authorizationUrl' ? value : 'https://placeholder.invalid', refreshToken: 'placeholder' }
|
|
699
|
+
: {})
|
|
700
|
+
}, 'environment');
|
|
701
|
+
return true;
|
|
702
|
+
} catch (error) {
|
|
703
|
+
return String(error?.message || '').startsWith(FIELD_LABELS[field]) ? error.message : true;
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
const tenantUrl = await textPrompt({
|
|
707
|
+
message: 'Datasphere tenant URL (for example https://<tenant>.eu10.hcs.cloud.sap)',
|
|
708
|
+
validate: validateUrl('url')
|
|
709
|
+
}, { input, output });
|
|
710
|
+
const tokenUrl = await textPrompt({
|
|
711
|
+
message: 'Token URL',
|
|
712
|
+
validate: validateUrl('tokenUrl')
|
|
713
|
+
}, { input, output });
|
|
714
|
+
const clientId = await textPrompt({ message: 'OAuth client ID', secret: true }, { input, output });
|
|
715
|
+
const clientSecret = await textPrompt({ message: 'OAuth client secret', secret: true }, { input, output });
|
|
716
|
+
let authorizationUrl = '';
|
|
717
|
+
if (auth === 'interactive') {
|
|
718
|
+
authorizationUrl = await textPrompt({
|
|
719
|
+
message: 'Authorization URL',
|
|
720
|
+
validate: validateUrl('authorizationUrl')
|
|
721
|
+
}, { input, output });
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
let parsed;
|
|
725
|
+
try {
|
|
726
|
+
parsed = resolveDatasphereConfig({
|
|
727
|
+
...env,
|
|
728
|
+
DATASPHERE_URL: tenantUrl,
|
|
729
|
+
DATASPHERE_TOKEN_URL: tokenUrl,
|
|
730
|
+
DATASPHERE_CLIENT_ID: clientId,
|
|
731
|
+
DATASPHERE_CLIENT_SECRET: clientSecret,
|
|
732
|
+
DATASPHERE_AUTH: auth,
|
|
733
|
+
// The refresh token only exists after the sign-in below; a placeholder
|
|
734
|
+
// satisfies the interactive validation and is replaced by the login result.
|
|
735
|
+
...(auth === 'interactive' ? { DATASPHERE_AUTHORIZATION_URL: authorizationUrl, DATASPHERE_REFRESH_TOKEN: 'pending-sign-in' } : {})
|
|
736
|
+
});
|
|
737
|
+
} catch (error) {
|
|
738
|
+
print(output, formatStatus(error.message, 'warning', output, 'Datasphere'));
|
|
739
|
+
return false;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
let refreshToken = null;
|
|
743
|
+
let accessToken = null;
|
|
744
|
+
if (auth === 'interactive') {
|
|
745
|
+
const mode = await selectPrompt({
|
|
746
|
+
message: 'How should the sign-in finish?',
|
|
747
|
+
defaultValue: 'browser',
|
|
748
|
+
choices: [
|
|
749
|
+
{ name: 'Open the browser here (local machine)', value: 'browser', shortcut: 'b' },
|
|
750
|
+
{ name: 'Paste the redirected URL (BAS or remote machine)', value: 'paste', shortcut: 'p' }
|
|
751
|
+
]
|
|
752
|
+
}, { input, output });
|
|
753
|
+
try {
|
|
754
|
+
const session = await login({
|
|
755
|
+
config: parsed,
|
|
756
|
+
mode,
|
|
757
|
+
openUrl: url => {
|
|
758
|
+
print(output, formatStatus(`Sign in with this URL (it also opened in your browser): ${url}`, 'info', output, 'Datasphere'));
|
|
759
|
+
openInBrowser(url);
|
|
760
|
+
},
|
|
761
|
+
promptPaste: () => textPrompt({
|
|
762
|
+
message: 'Paste the full URL the browser was redirected to (or just the code)',
|
|
763
|
+
secret: true
|
|
764
|
+
}, { input, output })
|
|
765
|
+
});
|
|
766
|
+
refreshToken = session.refreshToken || null;
|
|
767
|
+
accessToken = session.accessToken || null;
|
|
768
|
+
} catch (error) {
|
|
769
|
+
print(output, formatStatus(`Datasphere sign-in failed: ${diagnosticText(error)}`, 'warning', output, 'Datasphere'));
|
|
770
|
+
return false;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const verification = await verifyDatasphere(parsed, { env, ...(accessToken ? { accessToken } : {}) });
|
|
775
|
+
if (!verification.ok) {
|
|
776
|
+
print(output, formatStatus(`Verification failed: ${verification.message}`, 'warning', output, 'Datasphere'));
|
|
777
|
+
if (!await confirmPrompt('Save these credentials anyway? (y/n, Enter=no)', { input, output, defaultYes: false })) return false;
|
|
778
|
+
} else {
|
|
779
|
+
print(output, formatStatus(`Datasphere ${verification.message}.`, 'success', output, 'Datasphere'));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
let sql = null;
|
|
783
|
+
if (await confirmPrompt('Add SQL access with a Datasphere database user (Open SQL schema)? (y/n, Enter=no)', { input, output, defaultYes: false })) {
|
|
784
|
+
const sqlHost = await textPrompt({ message: 'Database user host name' }, { input, output });
|
|
785
|
+
const sqlPort = await textPrompt({ message: 'Port (Enter=443)', placeholder: '443', required: false }, { input, output });
|
|
786
|
+
const sqlUser = await textPrompt({ message: 'Database user name' }, { input, output });
|
|
787
|
+
const sqlPassword = await textPrompt({ message: 'Database user password', secret: true }, { input, output });
|
|
788
|
+
const sqlSchema = await textPrompt({
|
|
789
|
+
message: `Schema (Enter=${sqlUser})`,
|
|
790
|
+
placeholder: sqlUser,
|
|
791
|
+
required: false
|
|
792
|
+
}, { input, output });
|
|
793
|
+
const port = Number(sqlPort || 443);
|
|
794
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
795
|
+
print(output, formatStatus('DATASPHERE_SQL_PORT must be an integer between 1 and 65535', 'warning', output, 'Datasphere'));
|
|
796
|
+
return false;
|
|
797
|
+
}
|
|
798
|
+
sql = { host: sqlHost, port, user: sqlUser, password: sqlPassword, schema: sqlSchema || sqlUser };
|
|
799
|
+
try {
|
|
800
|
+
const database = await connectHana(sql, { queryTimeoutMs: 15_000 });
|
|
801
|
+
try {
|
|
802
|
+
await database.query('SELECT CURRENT_USER FROM DUMMY');
|
|
803
|
+
} finally {
|
|
804
|
+
await database.close();
|
|
805
|
+
}
|
|
806
|
+
print(output, formatStatus(`SQL access verified (${sqlUser}@${sqlHost}).`, 'success', output, 'Datasphere'));
|
|
807
|
+
} catch (error) {
|
|
808
|
+
print(output, formatStatus(`SQL verification failed: ${diagnosticText(error)}`, 'warning', output, 'Datasphere'));
|
|
809
|
+
if (!await confirmPrompt('Save these credentials anyway? (y/n, Enter=no)', { input, output, defaultYes: false })) return false;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const record = {
|
|
814
|
+
auth: parsed.auth,
|
|
815
|
+
url: parsed.url,
|
|
816
|
+
tokenurl: parsed.tokenUrl,
|
|
817
|
+
clientid: parsed.clientId,
|
|
818
|
+
clientsecret: parsed.clientSecret,
|
|
819
|
+
...(parsed.auth === 'interactive' ? { authorizationurl: parsed.authorizationUrl, ...(refreshToken ? { refreshtoken: refreshToken } : {}) } : {}),
|
|
820
|
+
...(sql ? { sql } : {})
|
|
821
|
+
};
|
|
822
|
+
const path = await storeDatasphereCredentials(record, env);
|
|
823
|
+
print(output, formatStatus(`Saved to ${path}`, 'success', output, 'Datasphere'));
|
|
824
|
+
return true;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** `sap-ai-dev --setup-datasphere`: only the Datasphere step (sign in, verify, add the server). */
|
|
828
|
+
export async function runDatasphereSetup({
|
|
829
|
+
env = process.env,
|
|
830
|
+
input = stdin,
|
|
831
|
+
output = stdout,
|
|
832
|
+
install = installMcpConfig,
|
|
833
|
+
verifyDatasphere = defaultVerifyDatasphere,
|
|
834
|
+
login = authorizationCodeLogin
|
|
835
|
+
} = {}) {
|
|
836
|
+
if (!input.isTTY || !output.isTTY) {
|
|
837
|
+
print(output, formatStatus('Interactive setup needs a terminal. Rerun sap-ai-dev --setup-datasphere from a terminal.', 'warning', output, 'SAP AI Dev Toolkit'));
|
|
838
|
+
return { skipped: true, reason: 'non-tty' };
|
|
839
|
+
}
|
|
840
|
+
const ok = await promptDatasphere({ env, input, output, verifyDatasphere, login });
|
|
841
|
+
if (!ok) {
|
|
842
|
+
print(output, formatStatus('Datasphere setup cancelled; no MCP entry was added.', 'warning', output, 'SAP AI Dev Toolkit'));
|
|
843
|
+
return { skipped: true, reason: 'no-credentials' };
|
|
844
|
+
}
|
|
845
|
+
const result = await install([], { env, sapDevelopmentServers: ['datasphere'], additive: true });
|
|
846
|
+
print(output, '');
|
|
847
|
+
print(output, formatStatus(`Configured the SAP Datasphere MCP server${result?.path ? ` in ${result.path}` : ''}.`, 'success', output, 'SAP AI Dev Toolkit'));
|
|
848
|
+
for (const message of Array.isArray(result?.warnings) ? result.warnings : []) print(output, formatStatus(message, 'warning', output, 'Warning'));
|
|
849
|
+
return { selected: [], ...(result || {}) };
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function processAutomationCredentialStatus(env = process.env) {
|
|
853
|
+
try {
|
|
854
|
+
const config = resolveProcessAutomationConfig(env, { stored: readStoredProcessAutomationCredentials(env) });
|
|
855
|
+
const lines = [`SAP Build Process Automation API ${new URL(config.url).host} (${config.auth}, source: ${config.source})`];
|
|
856
|
+
return { found: true, source: config.source, auth: config.auth, config, lines };
|
|
857
|
+
} catch (error) {
|
|
858
|
+
return { found: false, source: null, auth: null, config: null, lines: [String(error?.message || '')] };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function defaultVerifyProcessAutomation(config, { env, accessToken } = {}) {
|
|
863
|
+
try {
|
|
864
|
+
await createProcessAutomationRestClient(config, { env, ...(accessToken ? { accessToken } : {}) })
|
|
865
|
+
.request('GET', '/public/workflow/rest/v1/workflow-definitions', { query: { $top: 1 } });
|
|
866
|
+
return { ok: true, message: 'signed in; Workflow API reachable' };
|
|
867
|
+
} catch (error) {
|
|
868
|
+
return { ok: false, message: diagnosticText(error) };
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
/** Prompt for a Process Automation service key, optionally sign in as the user, verify it online, and store it. */
|
|
873
|
+
export async function promptProcessAutomation({
|
|
874
|
+
env = process.env,
|
|
875
|
+
input = stdin,
|
|
876
|
+
output = stdout,
|
|
877
|
+
verifyProcessAutomation = defaultVerifyProcessAutomation,
|
|
878
|
+
login = authorizationCodeLogin
|
|
879
|
+
} = {}) {
|
|
880
|
+
const label = 'Process Automation';
|
|
881
|
+
print(output, '');
|
|
882
|
+
print(output, formatStatus('SAP Build Process Automation tools', 'step', output, label));
|
|
883
|
+
print(output, ' Connects your AI agent to SAP Build Process Automation through a service key of a process-automation-service instance. Credentials are checked online and stored only for you, never in mcp.json.');
|
|
884
|
+
const status = processAutomationCredentialStatus(env);
|
|
885
|
+
for (const line of status.lines) print(output, ` ${line}`);
|
|
886
|
+
if (status.found) {
|
|
887
|
+
if (status.source === 'environment') return true;
|
|
888
|
+
if (!await confirmPrompt('Replace the stored SAP Build Process Automation sign-in? (y/n, Enter=no)', { input, output, defaultYes: false })) return true;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const auth = await selectPrompt({
|
|
892
|
+
message: 'How should the toolkit sign in to SAP Build Process Automation?',
|
|
893
|
+
defaultValue: 'technical',
|
|
894
|
+
choices: [
|
|
895
|
+
{ name: 'Technical client (service key client credentials): processes and task decisions run as the service key\'s technical user', value: 'technical', shortcut: 't' },
|
|
896
|
+
{ name: 'Your user (sign in with the service key client): actions are attributed to you; needs ProcessAutomationAdmin or ProcessAutomationParticipant', value: 'interactive', shortcut: 'y' }
|
|
897
|
+
]
|
|
898
|
+
}, { input, output });
|
|
899
|
+
|
|
900
|
+
const pasted = await textPrompt({
|
|
901
|
+
message: 'Paste the Process Automation service key JSON or the path to its .json file',
|
|
902
|
+
secret: true,
|
|
903
|
+
validate: value => {
|
|
904
|
+
try {
|
|
905
|
+
parseProcessAutomationServiceKey(readPastedJson(value, env));
|
|
906
|
+
return true;
|
|
907
|
+
} catch (error) {
|
|
908
|
+
return String(error?.message || 'invalid service key');
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}, { input, output });
|
|
912
|
+
|
|
913
|
+
const interactive = auth === 'interactive';
|
|
914
|
+
let parsed;
|
|
915
|
+
try {
|
|
916
|
+
parsed = parseProcessAutomationCredentials({
|
|
917
|
+
auth,
|
|
918
|
+
...parseProcessAutomationServiceKey(readPastedJson(pasted, env)),
|
|
919
|
+
// The refresh token only exists after the sign-in below; a placeholder
|
|
920
|
+
// satisfies the interactive validation and is replaced by the login result.
|
|
921
|
+
...(interactive ? { refreshToken: 'pending-sign-in' } : {})
|
|
922
|
+
}, 'stored-key');
|
|
923
|
+
} catch (error) {
|
|
924
|
+
print(output, formatStatus(error.message, 'warning', output, label));
|
|
925
|
+
return false;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
let refreshToken = null;
|
|
929
|
+
let accessToken = null;
|
|
930
|
+
if (interactive) {
|
|
931
|
+
const mode = await selectPrompt({
|
|
932
|
+
message: 'How should the sign-in finish?',
|
|
933
|
+
defaultValue: 'paste',
|
|
934
|
+
choices: [
|
|
935
|
+
{ name: 'Paste the redirected URL (works with the service key\'s registered redirect)', value: 'paste', shortcut: 'p' },
|
|
936
|
+
{ name: 'Open the browser here with redirect to http://localhost:8080', value: 'browser', shortcut: 'b' }
|
|
937
|
+
]
|
|
938
|
+
}, { input, output });
|
|
939
|
+
try {
|
|
940
|
+
const session = await login({
|
|
941
|
+
config: parsed,
|
|
942
|
+
mode,
|
|
943
|
+
profile: PROCESS_AUTOMATION_OAUTH,
|
|
944
|
+
openUrl: url => {
|
|
945
|
+
print(output, formatStatus(`Sign in with this URL (it also opened in your browser): ${url}`, 'info', output, label));
|
|
946
|
+
openInBrowser(url);
|
|
947
|
+
},
|
|
948
|
+
promptPaste: () => textPrompt({
|
|
949
|
+
message: 'Paste the full URL the browser was redirected to (or just the code)',
|
|
950
|
+
secret: true
|
|
951
|
+
}, { input, output })
|
|
952
|
+
});
|
|
953
|
+
refreshToken = session.refreshToken || null;
|
|
954
|
+
accessToken = session.accessToken || null;
|
|
955
|
+
} catch (error) {
|
|
956
|
+
print(output, formatStatus(`SAP Build Process Automation sign-in failed: ${diagnosticText(error)}. Choose the technical client if the service key does not allow user sign-in.`, 'warning', output, label));
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const verification = await verifyProcessAutomation(parsed, { env, ...(accessToken ? { accessToken } : {}) });
|
|
962
|
+
if (!verification.ok) {
|
|
963
|
+
print(output, formatStatus(`Verification failed: ${verification.message}`, 'warning', output, label));
|
|
964
|
+
if (!await confirmPrompt('Save these credentials anyway? (y/n, Enter=no)', { input, output, defaultYes: false })) return false;
|
|
965
|
+
} else {
|
|
966
|
+
print(output, formatStatus(`SAP Build Process Automation ${verification.message}.`, 'success', output, label));
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
print(output, ' Automation triggers, automation jobs and BAM/job exports need an environment API key (Control Tower > Environments > API Keys, scopes such as TRIGGER_READ/TRIGGER_EXECUTE). Workflow, inbox, decision and visibility tools work without one.');
|
|
970
|
+
const apiKey = await textPrompt({
|
|
971
|
+
message: 'Environment API key (Enter=skip)',
|
|
972
|
+
secret: true,
|
|
973
|
+
required: false,
|
|
974
|
+
validate: value => (!value || (value.length <= 512 && !/[^\x21-\x7e]/.test(value)) ? true : 'The API key must be a single printable token')
|
|
975
|
+
}, { input, output });
|
|
976
|
+
const environmentId = await textPrompt({
|
|
977
|
+
message: 'Default environment id for API triggers and process starts (Enter=skip)',
|
|
978
|
+
required: false,
|
|
979
|
+
validate: value => (!value || /^[A-Za-z0-9_.-]{1,128}$/.test(value) ? true : 'Use 1-128 letters, digits, dots, dashes or underscores')
|
|
980
|
+
}, { input, output });
|
|
981
|
+
|
|
982
|
+
const record = {
|
|
983
|
+
auth: parsed.auth,
|
|
984
|
+
url: parsed.url,
|
|
985
|
+
tokenurl: parsed.tokenUrl,
|
|
986
|
+
clientid: parsed.clientId,
|
|
987
|
+
clientsecret: parsed.clientSecret,
|
|
988
|
+
...(interactive && refreshToken ? { refreshtoken: refreshToken } : {}),
|
|
989
|
+
...(apiKey?.trim() ? { apikey: apiKey.trim() } : {}),
|
|
990
|
+
...(environmentId?.trim() ? { environmentid: environmentId.trim() } : {})
|
|
991
|
+
};
|
|
992
|
+
const path = await storeProcessAutomationCredentials(record, env);
|
|
993
|
+
print(output, formatStatus(`Saved to ${path}`, 'success', output, label));
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/** `sap-ai-dev --setup-process-automation`: only the Process Automation step (service key, sign in, verify, add the server). */
|
|
998
|
+
export async function runProcessAutomationSetup({
|
|
999
|
+
env = process.env,
|
|
1000
|
+
input = stdin,
|
|
1001
|
+
output = stdout,
|
|
1002
|
+
install = installMcpConfig,
|
|
1003
|
+
verifyProcessAutomation = defaultVerifyProcessAutomation,
|
|
1004
|
+
login = authorizationCodeLogin
|
|
1005
|
+
} = {}) {
|
|
1006
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1007
|
+
print(output, formatStatus('Interactive setup needs a terminal. Rerun sap-ai-dev --setup-process-automation from a terminal.', 'warning', output, 'SAP AI Dev Toolkit'));
|
|
1008
|
+
return { skipped: true, reason: 'non-tty' };
|
|
1009
|
+
}
|
|
1010
|
+
const ok = await promptProcessAutomation({ env, input, output, verifyProcessAutomation, login });
|
|
1011
|
+
if (!ok) {
|
|
1012
|
+
print(output, formatStatus('SAP Build Process Automation setup cancelled; no MCP entry was added.', 'warning', output, 'SAP AI Dev Toolkit'));
|
|
1013
|
+
return { skipped: true, reason: 'no-credentials' };
|
|
1014
|
+
}
|
|
1015
|
+
const result = await install([], { env, sapDevelopmentServers: ['process-automation'], additive: true });
|
|
1016
|
+
print(output, '');
|
|
1017
|
+
print(output, formatStatus(`Configured the SAP Build Process Automation MCP server${result?.path ? ` in ${result.path}` : ''}.`, 'success', output, 'SAP AI Dev Toolkit'));
|
|
1018
|
+
for (const message of Array.isArray(result?.warnings) ? result.warnings : []) print(output, formatStatus(message, 'warning', output, 'Warning'));
|
|
1019
|
+
return { selected: [], ...(result || {}) };
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
async function defaultVerifyBtp({ cli, config, env }) {
|
|
1023
|
+
try {
|
|
1024
|
+
const runtime = createBtpCli({ cli, config, env });
|
|
1025
|
+
const version = await runtime.version();
|
|
1026
|
+
const account = await runtime.json(['get', 'accounts/global-account']);
|
|
1027
|
+
return { ok: true, message: `btp CLI${version ? ` ${version}` : ''} signed in to global account ${account?.displayName} (${account?.subdomain})` };
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
return { ok: false, loginRequired: error?.code === 'BTP_LOGIN_REQUIRED', message: diagnosticText(error) };
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** Locate the btp CLI, check (or start) its btp login --sso session, and remember the CLI location. */
|
|
1034
|
+
export async function promptBtp({
|
|
1035
|
+
env = process.env,
|
|
1036
|
+
input = stdin,
|
|
1037
|
+
output = stdout,
|
|
1038
|
+
verifyBtp = defaultVerifyBtp,
|
|
1039
|
+
loginBtp = loginBtpCli,
|
|
1040
|
+
locateBtp = locateBtpCli
|
|
1041
|
+
} = {}) {
|
|
1042
|
+
const label = 'SAP BTP';
|
|
1043
|
+
print(output, '');
|
|
1044
|
+
print(output, formatStatus('SAP BTP platform administration tools', 'step', output, label));
|
|
1045
|
+
print(output, ' Connects your AI agent to SAP BTP through your own btp CLI login (btp login --sso). The toolkit stores no credentials; it only remembers where the btp CLI is installed.');
|
|
1046
|
+
let saved;
|
|
1047
|
+
try {
|
|
1048
|
+
saved = readBtpSettings(env).cli;
|
|
1049
|
+
} catch {
|
|
1050
|
+
saved = undefined;
|
|
1051
|
+
}
|
|
1052
|
+
const cli = String(env.BTP_CLI || '').trim() || await locateBtp(env) || (typeof saved === 'string' ? saved.trim() : '');
|
|
1053
|
+
if (!cli) {
|
|
1054
|
+
print(output, formatStatus('The btp CLI was not found on PATH. Install the SAP BTP command line interface from https://tools.hana.ondemand.com/#cloud (or set BTP_CLI to its path), then rerun sap-ai-dev --setup-btp.', 'warning', output, label));
|
|
1055
|
+
return false;
|
|
1056
|
+
}
|
|
1057
|
+
const config = String(env.BTP_CLIENTCONFIG || '').trim() || null;
|
|
1058
|
+
print(output, ` btp CLI: ${cli}${config ? ` (config ${config})` : ''}`);
|
|
1059
|
+
|
|
1060
|
+
let verification = await verifyBtp({ cli, config, env });
|
|
1061
|
+
if (!verification.ok && verification.loginRequired
|
|
1062
|
+
&& await confirmPrompt('Sign in now with btp login --sso? (y/n, Enter=yes)', { input, output, defaultYes: true })) {
|
|
1063
|
+
try {
|
|
1064
|
+
await loginBtp({ cli, config, env });
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
print(output, formatStatus(`btp login failed: ${diagnosticText(error)}`, 'warning', output, label));
|
|
1067
|
+
}
|
|
1068
|
+
verification = await verifyBtp({ cli, config, env });
|
|
1069
|
+
}
|
|
1070
|
+
if (!verification.ok) {
|
|
1071
|
+
print(output, formatStatus(`Verification failed: ${verification.message}`, 'warning', output, label));
|
|
1072
|
+
if (!await confirmPrompt('Add the SAP BTP MCP server anyway? (y/n, Enter=no)', { input, output, defaultYes: false })) return false;
|
|
1073
|
+
} else {
|
|
1074
|
+
print(output, formatStatus(`SAP BTP ${verification.message}.`, 'success', output, label));
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const path = await storeBtpSettings({ cli, ...(config ? { config } : {}) }, env);
|
|
1078
|
+
print(output, formatStatus(`Saved the btp CLI location to ${path}`, 'success', output, label));
|
|
1079
|
+
return true;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** `sap-ai-dev --setup-btp`: only the SAP BTP step (locate the btp CLI, check the session, add the server). */
|
|
1083
|
+
export async function runBtpSetup({
|
|
1084
|
+
env = process.env,
|
|
1085
|
+
input = stdin,
|
|
1086
|
+
output = stdout,
|
|
1087
|
+
install = installMcpConfig,
|
|
1088
|
+
verifyBtp = defaultVerifyBtp,
|
|
1089
|
+
loginBtp = loginBtpCli,
|
|
1090
|
+
locateBtp = locateBtpCli
|
|
1091
|
+
} = {}) {
|
|
1092
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1093
|
+
print(output, formatStatus('Interactive setup needs a terminal. Rerun sap-ai-dev --setup-btp from a terminal.', 'warning', output, 'SAP AI Dev Toolkit'));
|
|
1094
|
+
return { skipped: true, reason: 'non-tty' };
|
|
1095
|
+
}
|
|
1096
|
+
const ok = await promptBtp({ env, input, output, verifyBtp, loginBtp, locateBtp });
|
|
1097
|
+
if (!ok) {
|
|
1098
|
+
print(output, formatStatus('SAP BTP setup cancelled; no MCP entry was added.', 'warning', output, 'SAP AI Dev Toolkit'));
|
|
1099
|
+
return { skipped: true, reason: 'not-configured' };
|
|
1100
|
+
}
|
|
1101
|
+
const result = await install([], { env, sapDevelopmentServers: ['sap-btp'], additive: true });
|
|
1102
|
+
print(output, '');
|
|
1103
|
+
print(output, formatStatus(`Configured the SAP BTP MCP server${result?.path ? ` in ${result.path}` : ''}.`, 'success', output, 'SAP AI Dev Toolkit'));
|
|
1104
|
+
for (const message of Array.isArray(result?.warnings) ? result.warnings : []) print(output, formatStatus(message, 'warning', output, 'Warning'));
|
|
1105
|
+
return { selected: [], ...(result || {}) };
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
export function cloudAlmCredentialStatus(env = process.env) {
|
|
1109
|
+
try {
|
|
1110
|
+
const config = resolveCloudAlmConfig(env, { stored: readStoredCloudAlmCredentials(env) });
|
|
1111
|
+
const lines = [`SAP Cloud ALM API ${new URL(config.url).host} (source: ${config.source})`];
|
|
1112
|
+
return { found: true, source: config.source, config, lines };
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
return { found: false, source: null, config: null, lines: [String(error?.message || '')] };
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async function defaultVerifyCloudAlm(config, { env } = {}) {
|
|
1119
|
+
const report = await verifyCloudAlmAccess(createCloudAlmRestClient(config, { env }));
|
|
1120
|
+
const failed = Object.entries(report).filter(([, result]) => String(result).startsWith('failed'));
|
|
1121
|
+
if (failed.length) return { ok: false, message: diagnosticText(failed.map(([name, result]) => `${name} ${result}`).join('; ')) };
|
|
1122
|
+
return { ok: true, message: 'Landscape and Analytics APIs reachable' };
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
/** Prompt for a Cloud ALM API service key, verify it online, and store it. */
|
|
1126
|
+
export async function promptCloudAlm({
|
|
1127
|
+
env = process.env,
|
|
1128
|
+
input = stdin,
|
|
1129
|
+
output = stdout,
|
|
1130
|
+
verifyCloudAlm = defaultVerifyCloudAlm
|
|
1131
|
+
} = {}) {
|
|
1132
|
+
const label = 'Cloud ALM';
|
|
1133
|
+
print(output, '');
|
|
1134
|
+
print(output, formatStatus('SAP Cloud ALM tools', 'step', output, label));
|
|
1135
|
+
print(output, ' Connects your AI agent to SAP Cloud ALM for operations through a service key of the "SAP Cloud ALM API" instance (plan standard) in your Cloud ALM subaccount. Credentials are checked online and stored only for you, never in mcp.json.');
|
|
1136
|
+
print(output, ' Grant the instance read scopes such as calm-api.landscape.read, calm-api.analytics.read, calm-api.logs.read, calm-api.exm.read, calm-api.integration-monitoring.read, calm-api.hm.read, calm-api.iep.read and calm-api.features.read; closing alerts also needs calm-api.event-situations.write.');
|
|
1137
|
+
const status = cloudAlmCredentialStatus(env);
|
|
1138
|
+
for (const line of status.lines) print(output, ` ${line}`);
|
|
1139
|
+
if (status.found) {
|
|
1140
|
+
if (status.source === 'environment') return true;
|
|
1141
|
+
if (!await confirmPrompt('Replace the stored SAP Cloud ALM service key? (y/n, Enter=no)', { input, output, defaultYes: false })) return true;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const pasted = await textPrompt({
|
|
1145
|
+
message: 'Paste the SAP Cloud ALM API service key JSON or the path to its .json file',
|
|
1146
|
+
secret: true,
|
|
1147
|
+
validate: value => {
|
|
1148
|
+
try {
|
|
1149
|
+
parseCloudAlmServiceKey(readPastedJson(value, env));
|
|
1150
|
+
return true;
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
return String(error?.message || 'invalid service key');
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}, { input, output });
|
|
1156
|
+
|
|
1157
|
+
let parsed;
|
|
1158
|
+
try {
|
|
1159
|
+
parsed = parseCloudAlmCredentials(parseCloudAlmServiceKey(readPastedJson(pasted, env)), 'stored-key');
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
print(output, formatStatus(error.message, 'warning', output, label));
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const verification = await verifyCloudAlm(parsed, { env });
|
|
1166
|
+
if (!verification.ok) {
|
|
1167
|
+
print(output, formatStatus(`Verification failed: ${verification.message}`, 'warning', output, label));
|
|
1168
|
+
if (!await confirmPrompt('Save these credentials anyway? (y/n, Enter=no)', { input, output, defaultYes: false })) return false;
|
|
1169
|
+
} else {
|
|
1170
|
+
print(output, formatStatus(`SAP Cloud ALM ${verification.message}.`, 'success', output, label));
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const path = await storeCloudAlmCredentials({
|
|
1174
|
+
url: parsed.url,
|
|
1175
|
+
tokenurl: parsed.tokenUrl,
|
|
1176
|
+
clientid: parsed.clientId,
|
|
1177
|
+
clientsecret: parsed.clientSecret
|
|
1178
|
+
}, env);
|
|
1179
|
+
print(output, formatStatus(`Saved to ${path}`, 'success', output, label));
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
/** `sap-ai-dev --setup-cloud-alm`: only the Cloud ALM step (service key, verify, add the server). */
|
|
1184
|
+
export async function runCloudAlmSetup({
|
|
1185
|
+
env = process.env,
|
|
1186
|
+
input = stdin,
|
|
1187
|
+
output = stdout,
|
|
1188
|
+
install = installMcpConfig,
|
|
1189
|
+
verifyCloudAlm = defaultVerifyCloudAlm
|
|
1190
|
+
} = {}) {
|
|
1191
|
+
if (!input.isTTY || !output.isTTY) {
|
|
1192
|
+
print(output, formatStatus('Interactive setup needs a terminal. Rerun sap-ai-dev --setup-cloud-alm from a terminal.', 'warning', output, 'SAP AI Dev Toolkit'));
|
|
1193
|
+
return { skipped: true, reason: 'non-tty' };
|
|
1194
|
+
}
|
|
1195
|
+
const ok = await promptCloudAlm({ env, input, output, verifyCloudAlm });
|
|
1196
|
+
if (!ok) {
|
|
1197
|
+
print(output, formatStatus('SAP Cloud ALM setup cancelled; no MCP entry was added.', 'warning', output, 'SAP AI Dev Toolkit'));
|
|
1198
|
+
return { skipped: true, reason: 'no-credentials' };
|
|
1199
|
+
}
|
|
1200
|
+
const result = await install([], { env, sapDevelopmentServers: ['cloud-alm'], additive: true });
|
|
1201
|
+
print(output, '');
|
|
1202
|
+
print(output, formatStatus(`Configured the SAP Cloud ALM MCP server${result?.path ? ` in ${result.path}` : ''}.`, 'success', output, 'SAP AI Dev Toolkit'));
|
|
1203
|
+
for (const message of Array.isArray(result?.warnings) ? result.warnings : []) print(output, formatStatus(message, 'warning', output, 'Warning'));
|
|
1204
|
+
return { selected: [], ...(result || {}) };
|
|
1205
|
+
}
|
|
1206
|
+
|
|
611
1207
|
function buildLocalClientDestination(destination, client, authMode, { multipleClients = false, duplicateSystemName = false, browserPath } = {}) {
|
|
612
1208
|
const name = localClientDestinationName(destination, client, { multipleClients, duplicateSystemName });
|
|
613
1209
|
const localDestination = { ...destination, name, serverName: name, client };
|
|
@@ -1121,9 +1717,13 @@ export async function runSetup({
|
|
|
1121
1717
|
sapDevelopmentServers = ['integration-suite'];
|
|
1122
1718
|
} else if (includeSapDevelopmentToolsPrompt) {
|
|
1123
1719
|
print(output, '');
|
|
1124
|
-
print(output, formatStatus('Optionally add companion MCP servers for full-stack SAP development, HANA inspection,
|
|
1720
|
+
print(output, formatStatus('Optionally add companion MCP servers for full-stack SAP development, HANA inspection, Integration Suite (CPI/API Management/Developer Hub), SAP Datasphere, SAP Build Process Automation, SAP BTP platform administration, and SAP Cloud ALM.', 'step', output, 'Tools'));
|
|
1125
1721
|
print(output, ' HANA Cloud inspector is read-only and uses HANA_RO_* or VCAP_SERVICES already present in the MCP host environment; credentials are not written to mcp.json.');
|
|
1126
1722
|
print(output, ' Integration Suite uses CPI/APIM/DEVHUB service keys from the MCP host environment or keys you paste next (verified online, saved for your user only); credentials are not written to mcp.json.');
|
|
1723
|
+
print(output, ' SAP Datasphere uses a Datasphere OAuth client from DATASPHERE_* in the MCP host environment or entered next (verified online, saved for your user only); credentials are not written to mcp.json.');
|
|
1724
|
+
print(output, ' SAP Build Process Automation uses a service key from PROCESS_AUTOMATION_* in the MCP host environment or pasted next (verified online, saved for your user only); credentials are not written to mcp.json.');
|
|
1725
|
+
print(output, ' SAP BTP platform administration uses your own btp CLI login (btp login --sso), checked next; the toolkit stores no credentials and writes none to mcp.json.');
|
|
1726
|
+
print(output, ' SAP Cloud ALM uses a service key from CLOUD_ALM_* in the MCP host environment or pasted next (verified online, saved for your user only); credentials are not written to mcp.json.');
|
|
1127
1727
|
print(output, ' Leave empty to only configure ABAP/ADT destination servers.');
|
|
1128
1728
|
print(output, '');
|
|
1129
1729
|
sapDevelopmentServers = await checkboxPrompt({
|
|
@@ -1139,6 +1739,18 @@ export async function runSetup({
|
|
|
1139
1739
|
if (sapDevelopmentServers.includes('integration-suite')) {
|
|
1140
1740
|
await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, integrationCloudFoundry, ask: false });
|
|
1141
1741
|
}
|
|
1742
|
+
if (sapDevelopmentServers.includes('datasphere') && !await promptDatasphere({ env, input, output })) {
|
|
1743
|
+
sapDevelopmentServers = sapDevelopmentServers.filter(id => id !== 'datasphere');
|
|
1744
|
+
}
|
|
1745
|
+
if (sapDevelopmentServers.includes('process-automation') && !await promptProcessAutomation({ env, input, output })) {
|
|
1746
|
+
sapDevelopmentServers = sapDevelopmentServers.filter(id => id !== 'process-automation');
|
|
1747
|
+
}
|
|
1748
|
+
if (sapDevelopmentServers.includes('sap-btp') && !await promptBtp({ env, input, output })) {
|
|
1749
|
+
sapDevelopmentServers = sapDevelopmentServers.filter(id => id !== 'sap-btp');
|
|
1750
|
+
}
|
|
1751
|
+
if (sapDevelopmentServers.includes('cloud-alm') && !await promptCloudAlm({ env, input, output })) {
|
|
1752
|
+
sapDevelopmentServers = sapDevelopmentServers.filter(id => id !== 'cloud-alm');
|
|
1753
|
+
}
|
|
1142
1754
|
} else if (await promptIntegrationSuite({ env, alreadyConfigured: integrationSuiteConfigured, input, output, verifyIntegration, integrationCloudFoundry })) {
|
|
1143
1755
|
// Without --tools, Integration Suite still gets its own yes/no step instead of being hidden.
|
|
1144
1756
|
sapDevelopmentServers = ['integration-suite'];
|