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.
Files changed (62) hide show
  1. package/.github/agents/sap-btp-infrastructure-admin.agent.md +23 -0
  2. package/.github/agents/sap-bw-datasphere-migration-specialist.agent.md +22 -0
  3. package/.github/agents/sap-datasphere-developer.agent.md +22 -0
  4. package/.github/agents/sap-devops-observability-specialist.agent.md +54 -0
  5. package/.github/agents/sap-process-automation-developer.agent.md +50 -0
  6. package/.github/skills/btp-platform-administration/SKILL.md +53 -0
  7. package/.github/skills/bw-datasphere-migration/SKILL.md +66 -0
  8. package/.github/skills/cloud-alm-operations/SKILL.md +135 -0
  9. package/.github/skills/datasphere-administration/SKILL.md +40 -0
  10. package/.github/skills/datasphere-development/SKILL.md +55 -0
  11. package/.github/skills/process-automation-development/SKILL.md +113 -0
  12. package/NOTICE +2 -1
  13. package/README.md +152 -8
  14. package/dist/checksums.txt +5 -5
  15. package/dist/vsp-darwin-arm64 +0 -0
  16. package/dist/vsp-darwin-x64 +0 -0
  17. package/dist/vsp-linux-arm64 +0 -0
  18. package/dist/vsp-linux-x64 +0 -0
  19. package/dist/vsp-win32-x64.exe +0 -0
  20. package/inventory.md +60 -0
  21. package/package.json +8 -2
  22. package/patches/vsp-namespaced-class-include-edit.patch +134 -0
  23. package/scripts/build-vsp.mjs +3 -1
  24. package/src/btp-cli.mjs +199 -0
  25. package/src/btp-server.mjs +125 -0
  26. package/src/btp-tools.mjs +776 -0
  27. package/src/cloud-alm-client.mjs +44 -0
  28. package/src/cloud-alm-config.mjs +92 -0
  29. package/src/cloud-alm-credentials.mjs +29 -0
  30. package/src/cloud-alm-data.mjs +129 -0
  31. package/src/cloud-alm-server.mjs +115 -0
  32. package/src/cloud-alm-tools.mjs +682 -0
  33. package/src/datasphere-auth.mjs +279 -0
  34. package/src/datasphere-cli.mjs +220 -0
  35. package/src/datasphere-command.mjs +236 -0
  36. package/src/datasphere-config.mjs +154 -0
  37. package/src/datasphere-credentials.mjs +37 -0
  38. package/src/datasphere-csn.mjs +73 -0
  39. package/src/datasphere-discovery.mjs +151 -0
  40. package/src/datasphere-server.mjs +249 -0
  41. package/src/datasphere-tools.mjs +809 -0
  42. package/src/hana-database.mjs +3 -3
  43. package/src/hana-inspector.mjs +2 -2
  44. package/src/harnesses.mjs +31 -1
  45. package/src/install-user-assets.mjs +1 -1
  46. package/src/integration-common.mjs +7 -7
  47. package/src/integration-config.mjs +5 -0
  48. package/src/integration-credentials.mjs +25 -16
  49. package/src/integration-http.mjs +24 -7
  50. package/src/launcher.mjs +56 -4
  51. package/src/mcp-config.mjs +37 -1
  52. package/src/process-automation-automation-tools.mjs +125 -0
  53. package/src/process-automation-client.mjs +36 -0
  54. package/src/process-automation-common.mjs +69 -0
  55. package/src/process-automation-config.mjs +142 -0
  56. package/src/process-automation-credentials.mjs +37 -0
  57. package/src/process-automation-inbox-tools.mjs +246 -0
  58. package/src/process-automation-server.mjs +139 -0
  59. package/src/process-automation-tools.mjs +727 -0
  60. package/src/process-automation-visibility-tools.mjs +182 -0
  61. package/src/setup.mjs +623 -11
  62. package/tools.md +138 -0
@@ -0,0 +1,279 @@
1
+ // XSUAA OAuth helpers: token requests (SAP Datasphere's x-sap-sac-custom-auth
2
+ // convention by default; other XSUAA products through `profile`), an interactive
3
+ // (authorization code + refresh token) provider, and the localhost
4
+ // authorization-code login used by the sap-ai-dev setup wizards.
5
+ import { execFile } from 'node:child_process';
6
+ import { createServer } from 'node:http';
7
+ import { randomBytes } from 'node:crypto';
8
+ import { EnvHttpProxyAgent, fetch as undiciFetch } from 'undici';
9
+ import { IntegrationError, readCapped } from './integration-http.mjs';
10
+
11
+ const TOKEN_TIMEOUT_MS = 15_000;
12
+ const TOKEN_MAX_BYTES = 64 * 1024;
13
+ const REFRESH_MARGIN_MS = 15 * 60 * 1000;
14
+
15
+ /** Product profile: names in messages, the setup command, and whether the SAC custom-auth convention applies. */
16
+ export const DATASPHERE_OAUTH = Object.freeze({ product: 'Datasphere', setupCommand: 'sap-ai-dev --setup-datasphere', sacCustomAuth: true });
17
+
18
+ function proxyDispatcher(env) {
19
+ const httpProxy = env?.HTTP_PROXY || env?.http_proxy;
20
+ const httpsProxy = env?.HTTPS_PROXY || env?.https_proxy;
21
+ if (!httpProxy && !httpsProxy) return undefined;
22
+ return new EnvHttpProxyAgent({ httpProxy, httpsProxy, noProxy: env?.NO_PROXY || env?.no_proxy });
23
+ }
24
+
25
+ /**
26
+ * POST the tenant token endpoint (Basic auth, form encoded). Custom `sb-` clients
27
+ * send no client_id in the body and require x-sap-sac-custom-auth; the response
28
+ * body is never surfaced. Returns { accessToken, refreshToken, expiresAt }.
29
+ */
30
+ export async function requestDatasphereToken({
31
+ tokenUrl,
32
+ clientId,
33
+ clientSecret,
34
+ form,
35
+ fetchImpl = undiciFetch,
36
+ env = process.env,
37
+ timeoutMs = TOKEN_TIMEOUT_MS,
38
+ now = Date.now,
39
+ profile = DATASPHERE_OAUTH
40
+ }) {
41
+ const body = profile.sacCustomAuth
42
+ ? new URLSearchParams({
43
+ ...form,
44
+ response_type: 'token',
45
+ ...(clientId.startsWith('sb-') ? {} : { client_id: clientId })
46
+ })
47
+ : new URLSearchParams(form);
48
+ const controller = new AbortController();
49
+ const dispatcher = proxyDispatcher(env);
50
+ let timer;
51
+ try {
52
+ const request = async () => {
53
+ const response = await fetchImpl(tokenUrl, {
54
+ method: 'POST',
55
+ headers: {
56
+ 'content-type': 'application/x-www-form-urlencoded',
57
+ authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString('base64')}`,
58
+ ...(profile.sacCustomAuth && clientId.startsWith('sb-') ? { 'x-sap-sac-custom-auth': 'true' } : {})
59
+ },
60
+ body: body.toString(),
61
+ redirect: 'manual',
62
+ signal: controller.signal,
63
+ ...(dispatcher ? { dispatcher } : {})
64
+ });
65
+ if (response.status < 200 || response.status >= 300) {
66
+ void response.body?.cancel().catch(() => {});
67
+ throw new IntegrationError(`${profile.product} token request failed (HTTP ${response.status})`);
68
+ }
69
+ const bytes = await readCapped(response, TOKEN_MAX_BYTES, `${profile.product} token`, controller.signal);
70
+ let payload;
71
+ try {
72
+ payload = JSON.parse(new TextDecoder().decode(bytes));
73
+ } catch {
74
+ throw new IntegrationError(`${profile.product} token response was not valid JSON`);
75
+ }
76
+ if (typeof payload?.access_token !== 'string' || !payload.access_token) throw new IntegrationError(`${profile.product} token response contained no access token`);
77
+ const expiresInSeconds = Number(payload.expires_in);
78
+ return {
79
+ accessToken: payload.access_token,
80
+ refreshToken: typeof payload.refresh_token === 'string' && payload.refresh_token ? payload.refresh_token : undefined,
81
+ expiresAt: now() + (Number.isFinite(expiresInSeconds) ? expiresInSeconds : 0) * 1000
82
+ };
83
+ };
84
+ return await Promise.race([
85
+ request(),
86
+ new Promise((_resolve, reject) => {
87
+ timer = setTimeout(() => {
88
+ controller.abort();
89
+ reject(new IntegrationError(`${profile.product} token request failed (timeout)`));
90
+ }, timeoutMs);
91
+ })
92
+ ]);
93
+ } catch (error) {
94
+ if (error instanceof IntegrationError) throw error;
95
+ throw new IntegrationError(`${profile.product} token request failed (${error?.name === 'AbortError' ? 'timeout' : 'network error'})`);
96
+ } finally {
97
+ clearTimeout(timer);
98
+ void dispatcher?.close().catch(() => {});
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Single-flight token provider: technical clients use client_credentials;
104
+ * interactive clients refresh via grant_type=refresh_token
105
+ * when no token is cached or it expires within 15 minutes, keeps a rotated
106
+ * refresh token in memory, and hands it to persistRefreshToken.
107
+ */
108
+ export function createDatasphereTokenProvider({ config, persistRefreshToken, fetchImpl, env = process.env, now = Date.now, profile = DATASPHERE_OAUTH }) {
109
+ let current = null;
110
+ let refreshPromise = null;
111
+ const interactive = config.auth !== 'technical';
112
+ const sessionExpired = `${profile.product} interactive session expired or was revoked; rerun ${profile.setupCommand} to sign in again.`;
113
+
114
+ const refresh = async () => {
115
+ const refreshToken = current?.refreshToken || config.refreshToken;
116
+ if (interactive && !refreshToken) {
117
+ throw new IntegrationError(sessionExpired);
118
+ }
119
+ try {
120
+ const token = await requestDatasphereToken({
121
+ tokenUrl: config.tokenUrl,
122
+ clientId: config.clientId,
123
+ clientSecret: config.clientSecret,
124
+ form: interactive
125
+ ? { grant_type: 'refresh_token', refresh_token: refreshToken }
126
+ : { grant_type: 'client_credentials' },
127
+ ...(fetchImpl ? { fetchImpl } : {}),
128
+ env,
129
+ now,
130
+ profile
131
+ });
132
+ current = { ...token, refreshToken: token.refreshToken || refreshToken };
133
+ if (token.refreshToken && token.refreshToken !== refreshToken && persistRefreshToken) {
134
+ await persistRefreshToken(token.refreshToken);
135
+ }
136
+ return current;
137
+ } catch (error) {
138
+ if (!interactive) throw error;
139
+ if (error instanceof IntegrationError && error.message.startsWith(`${profile.product} interactive session`)) throw error;
140
+ throw new IntegrationError(sessionExpired);
141
+ }
142
+ };
143
+
144
+ return {
145
+ async token() {
146
+ if (current && current.expiresAt - now() >= REFRESH_MARGIN_MS) return current;
147
+ if (!refreshPromise) {
148
+ refreshPromise = refresh().finally(() => {
149
+ refreshPromise = null;
150
+ });
151
+ }
152
+ return refreshPromise;
153
+ }
154
+ };
155
+ }
156
+
157
+ export const createInteractiveTokenProvider = createDatasphereTokenProvider;
158
+
159
+ /** Best-effort platform browser open; every failure is ignored. */
160
+ export function openInBrowser(url, { execFileImpl = execFile } = {}) {
161
+ const platform = process.platform;
162
+ if (platform === 'darwin') execFileImpl('open', [url], () => {});
163
+ else if (platform === 'win32') execFileImpl('cmd', ['/c', 'start', '', url], () => {});
164
+ else execFileImpl('xdg-open', [url], () => {});
165
+ }
166
+
167
+ function extractCode(text, state, product) {
168
+ const trimmed = String(text || '').trim();
169
+ if (!trimmed) return '';
170
+ let url;
171
+ try { url = new URL(trimmed); } catch { return trimmed; }
172
+ if (url.searchParams.get('state') !== state) throw new IntegrationError(`${product} sign-in callback state did not match this login.`);
173
+ if (url.searchParams.has('error')) throw new IntegrationError(`${product} sign-in was denied or failed.`);
174
+ return url.searchParams.get('code') || '';
175
+ }
176
+
177
+ /**
178
+ * Interactive sign-in: build the authorize URL, capture the redirect `code`
179
+ * (browser mode on localhost:<port>, or a pasted URL/code), and exchange it.
180
+ */
181
+ export async function authorizationCodeLogin({
182
+ config,
183
+ mode,
184
+ port = 8080,
185
+ openUrl,
186
+ promptPaste,
187
+ timeoutMs = 120_000,
188
+ fetchImpl,
189
+ env = process.env,
190
+ now = Date.now,
191
+ profile = DATASPHERE_OAUTH
192
+ }) {
193
+ const product = profile.product;
194
+ const state = randomBytes(32).toString('hex');
195
+ const authorize = new URL(config.authorizationUrl);
196
+ authorize.searchParams.set('response_type', 'code');
197
+ authorize.searchParams.set('client_id', config.clientId);
198
+ authorize.searchParams.set('state', state);
199
+ const authorizeUrl = authorize.href;
200
+ const openAuthorizationUrl = () => {
201
+ try {
202
+ const opened = openUrl?.(authorizeUrl);
203
+ if (opened && typeof opened.then === 'function') opened.catch(() => {});
204
+ } catch {
205
+ // Opening the browser is best-effort; the caller can print the URL.
206
+ }
207
+ };
208
+ let code;
209
+ if (mode === 'browser') {
210
+ const server = createServer((request, response) => {
211
+ const callback = new URL(request.url || '/', `http://localhost:${port}`);
212
+ response.setHeader('content-type', 'text/html; charset=utf-8');
213
+ response.setHeader('cache-control', 'no-store');
214
+ response.setHeader('content-security-policy', "default-src 'none'; frame-ancestors 'none'");
215
+ if (request.method !== 'GET' || callback.pathname !== '/') {
216
+ response.writeHead(404);
217
+ response.end('Not found.');
218
+ return;
219
+ }
220
+ if (callback.searchParams.get('state') !== state) {
221
+ response.writeHead(400);
222
+ response.end(`This callback does not belong to the current ${product} login.`);
223
+ return;
224
+ }
225
+ if (callback.searchParams.has('error')) {
226
+ response.writeHead(400);
227
+ response.end(`${product} sign-in was denied or failed. Return to your terminal.`);
228
+ server.emit('datasphere-code', new IntegrationError(`${product} sign-in was denied or failed.`));
229
+ return;
230
+ }
231
+ const query = callback.searchParams.get('code');
232
+ if (!query) {
233
+ response.writeHead(400);
234
+ response.end(`No ${product} authorization code was received.`);
235
+ return;
236
+ }
237
+ response.writeHead(200);
238
+ response.end(`<!doctype html><html><body><p>Authorization received. Return to your terminal to confirm the ${product} connection.</p></body></html>`);
239
+ server.emit('datasphere-code', query);
240
+ });
241
+ try {
242
+ code = await new Promise((resolve, reject) => {
243
+ const finish = (error, value) => {
244
+ clearTimeout(timer);
245
+ server.removeListener('datasphere-code', onCode);
246
+ if (error) reject(error);
247
+ else resolve(value);
248
+ };
249
+ const onCode = value => value instanceof Error ? finish(value) : finish(null, value);
250
+ const timer = setTimeout(
251
+ () => finish(new IntegrationError(`No authorization code arrived within ${Math.round(timeoutMs / 1000)} s. Check that the OAuth client redirect URI is http://localhost:${port}.`)),
252
+ timeoutMs
253
+ );
254
+ server.on('datasphere-code', onCode);
255
+ server.on('error', error => finish(new IntegrationError(`${product} local login server could not listen on localhost:${port}: ${error?.code || 'error'}`)));
256
+ server.listen(port, 'localhost', openAuthorizationUrl);
257
+ });
258
+ } finally {
259
+ await new Promise(resolve => server.close(resolve)).catch(() => {});
260
+ }
261
+ } else if (mode === 'paste') {
262
+ if (typeof promptPaste !== 'function') throw new IntegrationError(`${product} paste sign-in needs a prompt`);
263
+ openAuthorizationUrl();
264
+ code = extractCode(await promptPaste(), state, product);
265
+ } else {
266
+ throw new IntegrationError(`${product} sign-in mode must be browser or paste`);
267
+ }
268
+ if (!code) throw new IntegrationError('No authorization code was found in the pasted URL or text.');
269
+ return requestDatasphereToken({
270
+ tokenUrl: config.tokenUrl,
271
+ clientId: config.clientId,
272
+ clientSecret: config.clientSecret,
273
+ form: { grant_type: 'authorization_code', code },
274
+ ...(fetchImpl ? { fetchImpl } : {}),
275
+ env,
276
+ now,
277
+ profile
278
+ });
279
+ }
@@ -0,0 +1,220 @@
1
+ // Child-process runner for @sap/datasphere-cli. The CLI is spawned (not driven
2
+ // through its in-process getCommands API) because cli-core joins and re-splits
3
+ // argv on spaces, which breaks values containing spaces. Secrets are written to
4
+ // a private state dir before every process; the child environment is an
5
+ // allowlist so CONSTANT_CASE host variables never leak into CLI options.
6
+ import { execFile } from 'node:child_process';
7
+ import { createRequire } from 'node:module';
8
+ import { readFileSync } from 'node:fs';
9
+ import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises';
10
+ import { dirname, join } from 'node:path';
11
+ import { tmpdir } from 'node:os';
12
+ import { randomUUID } from 'node:crypto';
13
+ import { IntegrationError } from './integration-http.mjs';
14
+ import { capText } from './integration-common.mjs';
15
+ import { diagnosticText } from './redact.mjs';
16
+
17
+ const require = createRequire(import.meta.url);
18
+
19
+ const CLI_TIMEOUT_MS = 180_000;
20
+ const MAX_BUFFER = 16 * 1024 * 1024;
21
+ const OUTPUT_CAP = 16 * 1024;
22
+ const ERROR_CAP = 800;
23
+ const CHILD_ENV_KEYS = [
24
+ 'PATH', 'Path', 'SystemRoot', 'SYSTEMROOT', 'TEMP', 'TMP', 'TMPDIR', 'LANG', 'LC_ALL',
25
+ 'HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy',
26
+ 'NODE_EXTRA_CA_CERTS', 'SSL_CERT_FILE', 'SSL_CERT_DIR'
27
+ ];
28
+
29
+ /** Isolate the SAP CLI cache and prevent host environment variables becoming CLI options. */
30
+ export function datasphereCliEnvironment(env, home) {
31
+ return {
32
+ ...Object.fromEntries(CHILD_ENV_KEYS.filter(key => env[key] !== undefined).map(key => [key, env[key]])),
33
+ HOME: home,
34
+ USERPROFILE: home
35
+ };
36
+ }
37
+
38
+ function loadCliPackage() {
39
+ const packageJsonPath = require.resolve('@sap/datasphere-cli/package.json');
40
+ return { metadata: JSON.parse(readFileSync(packageJsonPath, 'utf8')), entry: join(dirname(packageJsonPath), 'terminal.js') };
41
+ }
42
+
43
+ function flatten(options) {
44
+ const argv = [];
45
+ for (const [name, value] of Object.entries(options || {})) {
46
+ if (value === undefined || value === false) continue;
47
+ if (value === true) argv.push(`--${name}`);
48
+ else argv.push(`--${name}`, String(value));
49
+ }
50
+ return argv;
51
+ }
52
+
53
+ /**
54
+ * Create the Datasphere CLI runtime for one resolved config.
55
+ * `secrets()` is an async callback returning the OAuth record to hand to the CLI;
56
+ * it is rewritten to <stateDir>/secrets.json before every CLI process.
57
+ */
58
+ export function createDatasphereCli({
59
+ config,
60
+ secrets,
61
+ env = process.env,
62
+ execFileImpl = execFile,
63
+ tmpRoot = tmpdir()
64
+ } = {}) {
65
+ if (!config?.url) throw new Error('Datasphere CLI runner needs a resolved config');
66
+ if (typeof secrets !== 'function') throw new Error('Datasphere CLI runner needs a secrets provider');
67
+ let cli;
68
+ try {
69
+ cli = loadCliPackage();
70
+ } catch (error) {
71
+ throw new IntegrationError(`Datasphere CLI package could not be loaded: ${diagnosticText(error, 200)}`);
72
+ }
73
+ const { entry } = cli;
74
+
75
+ let stateDirPath = null;
76
+ let queue = Promise.resolve();
77
+ let initPromise = null;
78
+ let closed = false;
79
+ let lastSecrets = null;
80
+
81
+ const stateDir = () => stateDirPath;
82
+
83
+ const ensureState = async () => {
84
+ if (closed) throw new IntegrationError('Datasphere CLI runtime is closed');
85
+ if (!stateDirPath) stateDirPath = await mkdtemp(join(tmpRoot, 'sap-ai-datasphere-'));
86
+ return stateDirPath;
87
+ };
88
+
89
+ const childEnv = dir => datasphereCliEnvironment(env, dir);
90
+
91
+ const writeSecrets = async dir => {
92
+ const record = await secrets();
93
+ lastSecrets = record;
94
+ const path = join(dir, 'secrets.json');
95
+ await writeFile(path, `${JSON.stringify(record, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
96
+ await chmod(path, 0o600).catch(() => {});
97
+ return path;
98
+ };
99
+
100
+ const scrubValues = () => [
101
+ config.clientSecret,
102
+ config.clientId,
103
+ config.refreshToken,
104
+ config.sql?.password,
105
+ lastSecrets?.client_secret,
106
+ lastSecrets?.client_id,
107
+ lastSecrets?.access_token,
108
+ lastSecrets?.refresh_token
109
+ ].filter(Boolean);
110
+
111
+ const scrub = text => {
112
+ let out = String(text ?? '');
113
+ for (const value of new Set(scrubValues())) out = out.split(value).join('[redacted]');
114
+ return out;
115
+ };
116
+
117
+ const enqueue = operation => {
118
+ const result = queue.then(operation);
119
+ queue = result.then(() => undefined, () => undefined);
120
+ return result;
121
+ };
122
+
123
+ const exec = async (words, options = {}, { input } = {}) => {
124
+ const dir = await ensureState();
125
+ const secretsPath = await writeSecrets(dir);
126
+ let inputPath;
127
+ const argv = [entry, ...words, ...flatten(options), '--host', config.url, '--secrets-file', secretsPath];
128
+ if (input !== undefined) {
129
+ inputPath = join(dir, `input-${randomUUID()}.json`);
130
+ await writeFile(inputPath, JSON.stringify(input), { encoding: 'utf8', mode: 0o600 });
131
+ await chmod(inputPath, 0o600).catch(() => {});
132
+ argv.push('--file-path', inputPath);
133
+ }
134
+ try {
135
+ return await new Promise((resolve, reject) => {
136
+ const child = execFileImpl(process.execPath, argv, {
137
+ cwd: dir,
138
+ env: childEnv(dir),
139
+ timeout: CLI_TIMEOUT_MS,
140
+ maxBuffer: MAX_BUFFER,
141
+ windowsHide: true
142
+ }, (error, stdout, stderr) => {
143
+ if (error) {
144
+ if (error.killed) {
145
+ reject(new IntegrationError(`Datasphere CLI "${words.join(' ')}" timed out after ${CLI_TIMEOUT_MS / 1000} s`));
146
+ return;
147
+ }
148
+ const detail = String(stderr || stdout || error.message || '');
149
+ reject(new IntegrationError(`Datasphere CLI "${words.join(' ')}" failed: ${diagnosticText(scrub(detail), ERROR_CAP)}`));
150
+ return;
151
+ }
152
+ resolve(String(stdout ?? ''));
153
+ });
154
+ child?.stdin?.end();
155
+ });
156
+ } finally {
157
+ if (inputPath) await rm(inputPath, { force: true }).catch(() => {});
158
+ }
159
+ };
160
+
161
+ const shape = text => {
162
+ const trimmed = scrub(text).trim();
163
+ if (!trimmed) return null;
164
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
165
+ try {
166
+ return JSON.parse(trimmed);
167
+ } catch {
168
+ // Not JSON after all: fall through to plain text.
169
+ }
170
+ }
171
+ return { output: capText(trimmed, OUTPUT_CAP) };
172
+ };
173
+
174
+ const run = (words, options, runOptions) => enqueue(() => exec(words, options, runOptions)).then(shape);
175
+
176
+ const init = () => {
177
+ if (!initPromise) {
178
+ initPromise = enqueue(async () => {
179
+ try {
180
+ await exec(['config', 'cache', 'init']);
181
+ } catch {
182
+ await exec(['login'], { 'authorization-flow': config.auth === 'interactive' ? 'authorization_code' : 'client_credentials' });
183
+ }
184
+ });
185
+ }
186
+ return initPromise;
187
+ };
188
+
189
+ const version = () => enqueue(() => new Promise((resolve, reject) => {
190
+ const child = execFileImpl(process.execPath, [entry, '--version'], {
191
+ timeout: CLI_TIMEOUT_MS,
192
+ maxBuffer: MAX_BUFFER,
193
+ windowsHide: true
194
+ }, (error, stdout, stderr) => {
195
+ if (error) {
196
+ reject(new IntegrationError(`Datasphere CLI "--version" failed: ${diagnosticText(scrub(String(stderr || stdout || error.message || '')), ERROR_CAP)}`));
197
+ return;
198
+ }
199
+ resolve(String(stdout ?? '').split(/\r?\n/).map(line => line.trim()).find(line => /^\d/.test(line)) ?? '');
200
+ });
201
+ child?.stdin?.end();
202
+ }));
203
+
204
+ const close = async () => {
205
+ closed = true;
206
+ await queue.catch(() => {});
207
+ if (stateDirPath) await rm(stateDirPath, { recursive: true, force: true }).catch(() => {});
208
+ };
209
+
210
+ return {
211
+ get packageVersion() {
212
+ return cli.metadata.version;
213
+ },
214
+ init,
215
+ run,
216
+ version,
217
+ stateDir,
218
+ close
219
+ };
220
+ }