sap-ai-dev-toolkit 0.2.6 → 0.2.7
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/package.json +2 -2
- package/src/cf-destination.mjs +7 -10
- package/src/setup.mjs +7 -10
- package/src/terminal-ui.mjs +145 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-ai-dev-toolkit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, Fiori, UI5, and MCP",
|
|
5
5
|
"author": "Gurkan Yilmaz",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@abaplint/core": "2.119.0",
|
|
12
|
-
"
|
|
12
|
+
"terminal-kit": "^3.1.4",
|
|
13
13
|
"undici": "^6.21.3"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
package/src/cf-destination.mjs
CHANGED
|
@@ -2,11 +2,10 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { execFile as nodeExecFile } from 'node:child_process';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
import { stdin, stdout } from 'node:process';
|
|
5
|
-
import checkbox from '@inquirer/checkbox';
|
|
6
5
|
import { ProxyAgent, fetch as undiciFetch } from 'undici';
|
|
7
6
|
import { sanitizeChildEnv } from './bas-discovery.mjs';
|
|
8
7
|
import { createCfConnectivityProxy } from './cf-connectivity.mjs';
|
|
9
|
-
import {
|
|
8
|
+
import { checkboxPrompt, colorText } from './terminal-ui.mjs';
|
|
10
9
|
|
|
11
10
|
const REQUEST_TIMEOUT_MS = 10_000;
|
|
12
11
|
const COMMAND_TIMEOUT_MS = 30_000;
|
|
@@ -309,7 +308,7 @@ function destinationDescriptor(record, spaceGuid, instance, keyName, connectivit
|
|
|
309
308
|
|
|
310
309
|
async function chooseInstances(instances, { input = stdin, output = stdout, message }) {
|
|
311
310
|
if (instances.length === 1) return instances;
|
|
312
|
-
const selected = await
|
|
311
|
+
const selected = await checkboxPrompt({
|
|
313
312
|
message: colorText(`☁️ ${message}`, 'cyan', output),
|
|
314
313
|
choices: instances.map(instance => ({
|
|
315
314
|
value: instance,
|
|
@@ -317,22 +316,20 @@ async function chooseInstances(instances, { input = stdin, output = stdout, mess
|
|
|
317
316
|
checked: false
|
|
318
317
|
})),
|
|
319
318
|
required: false,
|
|
320
|
-
shortcuts: { all: 'a'
|
|
321
|
-
|
|
322
|
-
}, { input, output: promptOutput(output) });
|
|
319
|
+
shortcuts: { all: 'a' }
|
|
320
|
+
}, { input, output });
|
|
323
321
|
return selected;
|
|
324
322
|
}
|
|
325
323
|
|
|
326
324
|
async function chooseConnectivity(instances, { input = stdin, output = stdout }) {
|
|
327
325
|
if (instances.length === 1) return instances[0];
|
|
328
|
-
const selected = await
|
|
326
|
+
const selected = await checkboxPrompt({
|
|
329
327
|
message: colorText('🔌 Select one Connectivity service instance', 'cyan', output),
|
|
330
328
|
choices: instances.map(instance => ({ value: instance, name: `${instance.name || instance.guid} (${instance.guid})`, checked: false })),
|
|
331
329
|
required: false,
|
|
332
330
|
validate: values => values.length <= 1 || 'Select at most one Connectivity service instance',
|
|
333
|
-
shortcuts: { all: 'a'
|
|
334
|
-
|
|
335
|
-
}, { input, output: promptOutput(output) });
|
|
331
|
+
shortcuts: { all: 'a' }
|
|
332
|
+
}, { input, output });
|
|
336
333
|
return selected[0];
|
|
337
334
|
}
|
|
338
335
|
|
package/src/setup.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import checkbox from '@inquirer/checkbox';
|
|
2
1
|
import { createInterface } from 'node:readline/promises';
|
|
3
2
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { colorText, formatStatus
|
|
3
|
+
import { checkboxPrompt, colorText, formatStatus } from './terminal-ui.mjs';
|
|
5
4
|
import { discoverDestinations, remediation } from './bas-discovery.mjs';
|
|
6
5
|
import { discoverCloudFoundryDestinations, getCloudFoundryTarget, deleteManagedCloudFoundryServiceKeys } from './cf-destination.mjs';
|
|
7
6
|
import { SAP_DEVELOPMENT_MCP_SERVERS, collectCloudFoundryKeyReferencesFromAllEntries, collectManagedCloudFoundryKeyReferences, installMcpConfig, readMcpConfig, resolveMcpConfigPath } from './mcp-config.mjs';
|
|
@@ -205,13 +204,12 @@ export async function runSetup({
|
|
|
205
204
|
print(output, ' Space = select/deselect · a = toggle all · Enter = confirm.');
|
|
206
205
|
print(output, ' Nothing selected removes this add-on’s MCP entries.');
|
|
207
206
|
print(output, '');
|
|
208
|
-
const selected = await
|
|
207
|
+
const selected = await checkboxPrompt({
|
|
209
208
|
message: colorText('🧭 Select destinations', 'cyan', output),
|
|
210
209
|
choices,
|
|
211
210
|
required: false,
|
|
212
|
-
shortcuts: { all: 'a'
|
|
213
|
-
|
|
214
|
-
}, { input, output: promptOutput(output) });
|
|
211
|
+
shortcuts: { all: 'a' }
|
|
212
|
+
}, { input, output });
|
|
215
213
|
|
|
216
214
|
let sapDevelopmentServers = [];
|
|
217
215
|
if (includeSapDevelopmentToolsPrompt) {
|
|
@@ -219,7 +217,7 @@ export async function runSetup({
|
|
|
219
217
|
print(output, formatStatus('Optionally add companion MCP servers for full-stack SAP development.', 'step', output, 'Tools'));
|
|
220
218
|
print(output, ' Recommended for RAP/Fiori/CAP/UI validation work. Leave empty to only configure ABAP/ADT destination servers.');
|
|
221
219
|
print(output, '');
|
|
222
|
-
sapDevelopmentServers = await
|
|
220
|
+
sapDevelopmentServers = await checkboxPrompt({
|
|
223
221
|
message: colorText('🧰 Select companion tools', 'cyan', output),
|
|
224
222
|
choices: SAP_DEVELOPMENT_MCP_SERVERS.map(server => ({
|
|
225
223
|
value: server.id,
|
|
@@ -227,9 +225,8 @@ export async function runSetup({
|
|
|
227
225
|
checked: false
|
|
228
226
|
})),
|
|
229
227
|
required: false,
|
|
230
|
-
shortcuts: { all: 'a'
|
|
231
|
-
|
|
232
|
-
}, { input, output: promptOutput(output) });
|
|
228
|
+
shortcuts: { all: 'a' }
|
|
229
|
+
}, { input, output });
|
|
233
230
|
}
|
|
234
231
|
try {
|
|
235
232
|
const result = await install(selected, { env, sapDevelopmentServers });
|
package/src/terminal-ui.mjs
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { Writable } from 'node:stream';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import terminalKit from 'terminal-kit';
|
|
4
|
+
|
|
5
|
+
const { stringWidth, truncateString } = terminalKit;
|
|
2
6
|
|
|
3
7
|
const COLORS = {
|
|
4
8
|
cyan: '\u001b[1;36m',
|
|
@@ -65,3 +69,144 @@ export function promptOutput(output) {
|
|
|
65
69
|
export const quietSpinnerTheme = {
|
|
66
70
|
spinner: { interval: 80, frames: [''] }
|
|
67
71
|
};
|
|
72
|
+
|
|
73
|
+
function enabledChoices(choices) {
|
|
74
|
+
return choices.filter(choice => !choice.disabled);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function nextEnabledIndex(choices, start, step) {
|
|
78
|
+
if (!choices.length) return 0;
|
|
79
|
+
let index = start;
|
|
80
|
+
for (let count = 0; count < choices.length; count += 1) {
|
|
81
|
+
index = (index + step + choices.length) % choices.length;
|
|
82
|
+
if (!choices[index]?.disabled) return index;
|
|
83
|
+
}
|
|
84
|
+
return start;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function firstEnabledIndex(choices) {
|
|
88
|
+
const index = choices.findIndex(choice => !choice.disabled);
|
|
89
|
+
return index === -1 ? 0 : index;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function stripAnsi(value) {
|
|
93
|
+
return String(value).replace(/\u001b\[[0-9;]*m/g, '');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function clamp(value, min, max) {
|
|
97
|
+
return Math.max(min, Math.min(max, value));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function truncateToColumns(text, columns) {
|
|
101
|
+
const value = String(text);
|
|
102
|
+
if (columns <= 0 || stringWidth(value) <= columns) return value;
|
|
103
|
+
return truncateString(value, Math.max(1, columns));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function checkboxPageSize(output, choiceCount) {
|
|
107
|
+
const rows = Number.isFinite(output?.rows) ? output.rows : 24;
|
|
108
|
+
return clamp(Math.min(choiceCount, rows - 7), 1, Math.max(1, choiceCount));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function checkboxPrompt({ message, choices, required = false, shortcuts = { all: 'a' }, validate } = {}, { input = process.stdin, output = process.stdout } = {}) {
|
|
112
|
+
if (!Array.isArray(choices) || !choices.length) return [];
|
|
113
|
+
const selectable = enabledChoices(choices);
|
|
114
|
+
const selected = new Set(choices.flatMap((choice, index) => choice.checked && !choice.disabled ? [index] : []));
|
|
115
|
+
let cursor = firstEnabledIndex(choices);
|
|
116
|
+
let top = 0;
|
|
117
|
+
let renderedLines = 0;
|
|
118
|
+
let done = false;
|
|
119
|
+
let previousRawMode;
|
|
120
|
+
|
|
121
|
+
const write = text => output.write(text);
|
|
122
|
+
const clearRendered = () => {
|
|
123
|
+
if (!renderedLines) return;
|
|
124
|
+
write(`\u001b[${renderedLines}A\u001b[J`);
|
|
125
|
+
renderedLines = 0;
|
|
126
|
+
};
|
|
127
|
+
const ensureVisible = pageSize => {
|
|
128
|
+
if (cursor < top) top = cursor;
|
|
129
|
+
if (cursor >= top + pageSize) top = cursor - pageSize + 1;
|
|
130
|
+
top = clamp(top, 0, Math.max(0, choices.length - pageSize));
|
|
131
|
+
};
|
|
132
|
+
const render = (error = '') => {
|
|
133
|
+
const columns = Number.isFinite(output?.columns) ? output.columns : 80;
|
|
134
|
+
const pageSize = checkboxPageSize(output, choices.length);
|
|
135
|
+
ensureVisible(pageSize);
|
|
136
|
+
const lines = [];
|
|
137
|
+
lines.push(`${message} (Space: select, ${shortcuts?.all || 'a'}: toggle all, Enter: confirm)`);
|
|
138
|
+
const end = Math.min(choices.length, top + pageSize);
|
|
139
|
+
for (let index = top; index < end; index += 1) {
|
|
140
|
+
const choice = choices[index];
|
|
141
|
+
const pointer = index === cursor ? '❯' : ' ';
|
|
142
|
+
const marker = selected.has(index) ? '◉' : '◯';
|
|
143
|
+
const disabled = choice.disabled ? ` — ${choice.disabled}` : '';
|
|
144
|
+
const label = `${pointer}${marker} ${choice.name}${disabled}`;
|
|
145
|
+
lines.push(truncateToColumns(label, columns));
|
|
146
|
+
}
|
|
147
|
+
if (choices.length > pageSize) {
|
|
148
|
+
lines.push(colorText(` Showing ${top + 1}-${end} of ${choices.length}; use ↑/↓ to scroll.`, 'cyan', output));
|
|
149
|
+
}
|
|
150
|
+
if (error) lines.push(colorText(` ${error}`, 'red', output));
|
|
151
|
+
clearRendered();
|
|
152
|
+
write(`${lines.join('\n')}\n`);
|
|
153
|
+
renderedLines = lines.length;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
const cleanup = () => {
|
|
158
|
+
input.off('keypress', onKeypress);
|
|
159
|
+
if (typeof input.setRawMode === 'function' && previousRawMode !== undefined) input.setRawMode(previousRawMode);
|
|
160
|
+
if (typeof input.pause === 'function') input.pause();
|
|
161
|
+
write('\u001b[?25h');
|
|
162
|
+
};
|
|
163
|
+
const finish = value => {
|
|
164
|
+
if (done) return;
|
|
165
|
+
done = true;
|
|
166
|
+
clearRendered();
|
|
167
|
+
write(`${stripAnsi(message)} ${value.length ? `${value.length} selected` : 'none selected'}\n`);
|
|
168
|
+
cleanup();
|
|
169
|
+
resolve(value);
|
|
170
|
+
};
|
|
171
|
+
const fail = error => {
|
|
172
|
+
if (done) return;
|
|
173
|
+
done = true;
|
|
174
|
+
cleanup();
|
|
175
|
+
reject(error);
|
|
176
|
+
};
|
|
177
|
+
const onKeypress = (_chunk, key = {}) => {
|
|
178
|
+
if (key.ctrl && key.name === 'c') return fail(new Error('Prompt interrupted'));
|
|
179
|
+
if (key.name === 'up' || key.name === 'k') cursor = nextEnabledIndex(choices, cursor, -1);
|
|
180
|
+
else if (key.name === 'down' || key.name === 'j') cursor = nextEnabledIndex(choices, cursor, 1);
|
|
181
|
+
else if (key.name === 'space' && !choices[cursor]?.disabled) {
|
|
182
|
+
if (selected.has(cursor)) selected.delete(cursor);
|
|
183
|
+
else selected.add(cursor);
|
|
184
|
+
} else if (shortcuts?.all && key.name === shortcuts.all) {
|
|
185
|
+
const allSelected = selectable.every(choice => selected.has(choices.indexOf(choice)));
|
|
186
|
+
selected.clear();
|
|
187
|
+
if (!allSelected) for (const choice of selectable) selected.add(choices.indexOf(choice));
|
|
188
|
+
} else if (key.name === 'return' || key.name === 'enter') {
|
|
189
|
+
const values = [...selected].sort((a, b) => a - b).map(index => choices[index].value);
|
|
190
|
+
const validation = typeof validate === 'function' ? validate(values) : true;
|
|
191
|
+
if (validation !== true) return render(String(validation));
|
|
192
|
+
if (required && !values.length) return render('Select at least one item.');
|
|
193
|
+
return finish(values);
|
|
194
|
+
} else {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
render();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
readline.emitKeypressEvents(input);
|
|
202
|
+
previousRawMode = input.isRaw;
|
|
203
|
+
if (typeof input.setRawMode === 'function') input.setRawMode(true);
|
|
204
|
+
if (typeof input.resume === 'function') input.resume();
|
|
205
|
+
write('\u001b[?25l');
|
|
206
|
+
input.on('keypress', onKeypress);
|
|
207
|
+
render();
|
|
208
|
+
} catch (error) {
|
|
209
|
+
fail(error);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}
|