sap-ai-dev-toolkit 0.2.5 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.2.5",
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
- "@inquirer/checkbox": "4.2.0",
12
+ "terminal-kit": "^3.1.4",
13
13
  "undici": "^6.21.3"
14
14
  },
15
15
  "bin": {
@@ -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 { colorText, promptOutput, quietSpinnerTheme } from './terminal-ui.mjs';
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 checkbox({
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', invert: null },
321
- theme: quietSpinnerTheme
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 checkbox({
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', invert: null },
334
- theme: quietSpinnerTheme
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, promptOutput, quietSpinnerTheme } from './terminal-ui.mjs';
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 checkbox({
207
+ const selected = await checkboxPrompt({
209
208
  message: colorText('🧭 Select destinations', 'cyan', output),
210
209
  choices,
211
210
  required: false,
212
- shortcuts: { all: 'a', invert: null },
213
- theme: quietSpinnerTheme
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 checkbox({
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', invert: null },
231
- theme: quietSpinnerTheme
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 });
@@ -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
+ }