sap-ai-dev-toolkit 0.9.9 → 0.9.91

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.
@@ -1,5 +1,5 @@
1
- a5b602573f54ce3d03d161aaaeb78559fd73bd51f2ae9a3b7549cee1ddc3fbf6 vsp-linux-x64
2
- 2e336e2999c3654e5eb1e3a8bb42616eb5d5e422ab7ae74ffbff4634be45189d vsp-linux-arm64
3
- afc7fd0f2e40ae5ebfcd3ddb8ac3cb7e7501b2b66d76ac96e3b6054b09366bb5 vsp-darwin-x64
4
- 2d28a870c4a006900eec245384b4bbc2e84651e0ba911cca6107c132ace73575 vsp-darwin-arm64
5
- 7d0189aa7ff936a145b0e3f15bffce8de5aebc252465a720edc3a9b122bd2a28 vsp-win32-x64.exe
1
+ 02831a47fa28a1807d94f6b8485c21f1221c7b3aa0a2b9b7bf0682ab4fb5618f vsp-linux-x64
2
+ 4398dac4c9d304d63e03abbb86523b61e5180b5b355dd1a2ed0d2405454cc8f7 vsp-linux-arm64
3
+ 356a10913b7a46a8ed67132e9b15ea7ff92b8adeb1c799111866e9af176e85fe vsp-darwin-x64
4
+ fb5e72a59618578187eb8e4374571b32baab34397766fc6ea8ab328c6cea849b vsp-darwin-arm64
5
+ 50d5c74194063bc9bcc641fb580f1c1232cce0c1577cdad6dc503f41efd3344c vsp-win32-x64.exe
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sap-ai-dev-toolkit",
3
- "version": "0.9.9",
3
+ "version": "0.9.91",
4
4
  "description": "SAP AI development toolkit for BAS, ABAP, RAP, CAP, HANA Cloud, Fiori, UI5, and MCP",
5
5
  "author": "Gurkan Yilmaz",
6
6
  "type": "module",
@@ -492,3 +492,56 @@ index 0000000..80741da
492
492
  + }
493
493
  +}
494
494
  +
495
+ diff --git a/pkg/adt/sso.go b/pkg/adt/sso.go
496
+ index d67c4e8..4e9dc45 100644
497
+ --- a/pkg/adt/sso.go
498
+ +++ b/pkg/adt/sso.go
499
+ @@ -155,7 +155,7 @@ func CaptureSSOCookies(ctx context.Context, opts SSOOptions) (map[string]string,
500
+ // below is the real verdict.
501
+ if err := chromedp.Run(timeoutCtx, chromedp.Navigate(opts.TriggerURL)); err != nil {
502
+ if timeoutCtx.Err() != nil {
503
+ - return nil, fmt.Errorf("SSO capture timed out after %s", timeout)
504
+ + return nil, navigationTimeout(opts.Headless, timeout)
505
+ }
506
+ if opts.Verbose {
507
+ fmt.Fprintf(os.Stderr, "[SSO] navigation reported %v (normal during SSO)\n", err)
508
+ @@ -174,6 +174,17 @@ func CaptureSSOCookies(ctx context.Context, opts SSOOptions) (map[string]string,
509
+ return cookies, nil
510
+ }
511
+
512
+ +// navigationTimeout explains a first page that never finished loading. In a
513
+ +// silent browser that page is waiting on a person — a smart-card certificate
514
+ +// picker or a login prompt headless mode cannot show — so a visible window is
515
+ +// the next step, not a failed start.
516
+ +func navigationTimeout(headless bool, timeout time.Duration) error {
517
+ + if headless {
518
+ + return fmt.Errorf("%w: the silent sign-in page did not finish loading within %s", ErrSSOInteractiveRequired, timeout)
519
+ + }
520
+ + return fmt.Errorf("SSO capture timed out after %s", timeout)
521
+ +}
522
+ +
523
+ // DefaultSSOProfileDir returns a per-host browser profile directory under the
524
+ // user's cache location. Keeping one profile per host means a silent refresh
525
+ // for one system is never disturbed by a login to another.
526
+ diff --git a/pkg/adt/sso_navigation_test.go b/pkg/adt/sso_navigation_test.go
527
+ new file mode 100644
528
+ index 0000000..c851021
529
+ --- /dev/null
530
+ +++ b/pkg/adt/sso_navigation_test.go
531
+ @@ -0,0 +1,16 @@
532
+ +package adt
533
+ +
534
+ +import (
535
+ + "errors"
536
+ + "testing"
537
+ + "time"
538
+ +)
539
+ +
540
+ +func TestSilentNavigationTimeoutAsksForAPerson(t *testing.T) {
541
+ + if err := navigationTimeout(true, time.Second); !errors.Is(err, ErrSSOInteractiveRequired) {
542
+ + t.Fatalf("silent timeout = %v, want ErrSSOInteractiveRequired so a visible window opens", err)
543
+ + }
544
+ + if err := navigationTimeout(false, time.Second); errors.Is(err, ErrSSOInteractiveRequired) {
545
+ + t.Fatalf("headed timeout = %v, must not ask for yet another window", err)
546
+ + }
547
+ +}
package/src/launcher.mjs CHANGED
@@ -222,7 +222,14 @@ async function rememberAdtUrl(configured, found) {
222
222
  // swept only when the configuration never named one.
223
223
  async function resolveLocalAdtUrl(name, configured, client) {
224
224
  const log = message => logLine(`[sap-ai-dev] ${name}: ${message}`);
225
- const detect = target => detectLocalAdtUrl({ client, ...target }).catch(error => ({ url: '', reason: error.message, failed: true }));
225
+ const detect = async target => {
226
+ const started = Date.now();
227
+ // MCP output channels cannot animate, so a long scan reports that it is still running.
228
+ const heartbeat = setInterval(() => log(`port scan still running on ${new URL(target.url).hostname} (${Math.round((Date.now() - started) / 1000)} s)`), 20_000);
229
+ try { return await detectLocalAdtUrl({ client, ...target }); }
230
+ catch (error) { return { url: '', reason: error.message, failed: true }; }
231
+ finally { clearInterval(heartbeat); }
232
+ };
226
233
  const explicit = hasExplicitPort(configured);
227
234
  let verdict = { url: '', reason: '' };
228
235
  if (explicit) {
@@ -461,8 +461,10 @@ export function adtVerdict(result, protocol = 'https:') {
461
461
  }
462
462
 
463
463
  // A portless URL means 443, which on-premise ICMs rarely serve; VSP's `detect`
464
- // probes the conventional ADT ports and reports which one answers.
465
- export async function detectAdtUrl(binary, { url, client, instance, env = process.env, exhaustive = false, ports = [], timeoutMs = 180_000 } = {}) {
464
+ // probes the conventional ADT ports and reports which one answers. A host that
465
+ // accepts every port makes each probe wait out its HTTP timeouts, so the full
466
+ // sweep gets minutes.
467
+ export async function detectAdtUrl(binary, { url, client, instance, env = process.env, exhaustive = false, ports = [], timeoutMs = exhaustive ? 600_000 : 120_000 } = {}) {
466
468
  let configured;
467
469
  try { configured = new URL(url); } catch { return { url: '', reason: `${url} is not a valid URL` }; }
468
470
  const host = configured.hostname.replace(/^\[(.*)\]$/, '$1');
package/src/setup.mjs CHANGED
@@ -141,30 +141,42 @@ export function adtCandidate(answer, baseUrl) {
141
141
  // Cheapest evidence first: named ports get one probe, then the host's usual
142
142
  // ports, then the message server's application servers, then every port.
143
143
  async function findAdtUrl(destination, label, client, { candidates = [], scan = true, output = stdout, env = process.env, detectAdtUrl } = {}) {
144
- const detect = async (message, target) => {
145
- print(output, formatStatus(message, 'progress', output, 'ADT'));
146
- const stopProgress = startProgress(message, { output, env, label: 'ADT' });
144
+ const detect = async (line, activity, target) => {
145
+ print(output, formatStatus(line, 'progress', output, 'ADT'));
146
+ const started = Date.now();
147
+ const progress = startProgress(activity, { output, env, label: 'ADT' });
148
+ // A short, ticking spinner: a long line would truncate the animation away and look frozen.
149
+ const ticker = setInterval(() => progress.update(`${activity} (${Math.round((Date.now() - started) / 1000)} s)`), 1000);
147
150
  try { return await detectAdtUrl({ client, ...target }); }
148
151
  catch (error) { return { url: '', reason: error.message }; }
149
- finally { stopProgress(); }
152
+ finally {
153
+ clearInterval(ticker);
154
+ progress();
155
+ }
150
156
  };
151
157
  let verdict = { url: '', reason: `no port was checked for ${label}` };
152
158
  for (const candidate of [...new Set([destination.url, ...candidates].filter(url => url && hasExplicitPort(url)))]) {
153
- verdict = await detect(`Checking ADT at ${candidate}`, { url: candidate, ports: [adtPort(candidate)] });
159
+ verdict = await detect(`Checking ADT at ${candidate}`, `Checking ${candidate}`, { url: candidate, ports: [adtPort(candidate)] });
154
160
  if (verdict.url || verdict.unresolved) return verdict;
155
161
  }
156
162
  if (!scan) return verdict;
157
163
  const host = new URL(destination.url).hostname;
158
- verdict = await detect(`Port scan in progress on ${host} to find the ADT port for ${label}`, { url: destination.url, ...(destination.instance ? { instance: destination.instance } : {}) });
164
+ verdict = await detect(`Port scan in progress on ${host} to find the ADT port for ${label}`, `Scanning the usual ADT ports on ${host}`, { url: destination.url, ...(destination.instance ? { instance: destination.instance } : {}) });
159
165
  if (verdict.url || verdict.unresolved) return verdict;
160
166
  if (destination.messageServer) {
161
- print(output, formatStatus(`Asking the message server ${destination.messageServer.host}:${destination.messageServer.httpPort} for the application servers of ${label}`, 'progress', output, 'ADT'));
162
- for (const server of (await messageServerAppServers({ ...destination.messageServer, systemId: destination.systemId })).slice(0, 3)) {
163
- const found = await detect(`Port scan in progress on application server ${server.host} (instance ${server.instance})`, { url: `${new URL(destination.url).protocol}//${server.host}`, instance: server.instance });
167
+ const { host: messageHost, httpPort } = destination.messageServer;
168
+ print(output, formatStatus(`Asking the message server ${messageHost}:${httpPort} for the application servers of ${label}`, 'progress', output, 'ADT'));
169
+ const servers = (await messageServerAppServers({ ...destination.messageServer, systemId: destination.systemId })).slice(0, 3);
170
+ print(output, formatStatus(servers.length
171
+ ? `The message server lists ${servers.map(server => `${server.host} (instance ${server.instance})`).join(', ')}.`
172
+ : `The message server at ${messageHost}:${httpPort} listed no application servers.`, 'info', output, 'ADT'));
173
+ for (const server of servers) {
174
+ const found = await detect(`Port scan in progress on application server ${server.host} (instance ${server.instance})`, `Scanning ${server.host}`, { url: `${new URL(destination.url).protocol}//${server.host}`, instance: server.instance });
164
175
  if (found.url) return found;
165
176
  }
166
177
  }
167
- return detect(`No ADT port on the usual ports for ${label} (${verdict.reason}); port scan in progress on every conventional SAP port of ${host} (this can take a minute)`, { url: destination.url, exhaustive: true });
178
+ print(output, formatStatus(`No ADT port on the usual ports for ${label}: ${verdict.reason}.`, 'info', output, 'ADT'));
179
+ return detect(`Scanning the remaining conventional SAP ports on ${host} (about 300 ports; this can take a few minutes)`, `Scanning the remaining SAP ports on ${host}`, { url: destination.url, exhaustive: true });
168
180
  }
169
181
 
170
182
  // An SSO sign-in can only start where ADT answers, so for SSO setup does not