sap-ai-dev-toolkit 0.9.97 → 0.9.9904

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 (40) hide show
  1. package/.github/agents/abap-developer.agent.md +4 -2
  2. package/.github/agents/abap-runtime-debugger.agent.md +3 -1
  3. package/.github/agents/hana-cloud-hdi-specialist.agent.md +3 -2
  4. package/.github/agents/rap-service-developer.agent.md +6 -4
  5. package/.github/agents/sap-solution-architect.agent.md +5 -3
  6. package/.github/skills/abap-debugging/SKILL.md +1 -1
  7. package/.github/skills/abap-development/SKILL.md +7 -1
  8. package/.github/skills/abap-runtime-analysis/SKILL.md +1 -1
  9. package/.github/skills/abap-testing-quality/SKILL.md +4 -2
  10. package/.github/skills/cds-development/SKILL.md +5 -1
  11. package/.github/skills/clean-core-extensibility/SKILL.md +3 -1
  12. package/.github/skills/hana-cloud-native-development/SKILL.md +1 -1
  13. package/.github/skills/hana-cloud-validation/SKILL.md +1 -1
  14. package/.github/skills/rap-development/SKILL.md +6 -2
  15. package/.github/skills/rap-service-delivery/SKILL.md +3 -3
  16. package/.github/skills/sap-sdlc-orchestration/SKILL.md +2 -2
  17. package/.github/skills/sap-standard-api-analysis/SKILL.md +2 -0
  18. package/.github/skills/sap-transport-release/SKILL.md +1 -1
  19. package/README.md +62 -73
  20. package/dist/checksums.txt +5 -5
  21. package/dist/vsp-darwin-arm64 +0 -0
  22. package/dist/vsp-darwin-x64 +0 -0
  23. package/dist/vsp-linux-arm64 +0 -0
  24. package/dist/vsp-linux-x64 +0 -0
  25. package/dist/vsp-win32-x64.exe +0 -0
  26. package/inventory.md +25 -15
  27. package/package.json +1 -1
  28. package/patches/vsp-bas-proxy-auth.patch +342 -0
  29. package/scripts/build-vsp.mjs +2 -2
  30. package/scripts/postinstall.mjs +22 -11
  31. package/scripts/publish-npm.mjs +4 -4
  32. package/src/binary.mjs +14 -14
  33. package/src/launcher.mjs +19 -19
  34. package/src/local-sap-gui.mjs +46 -21
  35. package/src/mcp-config.mjs +35 -14
  36. package/src/mcp-proxy.mjs +26 -16
  37. package/src/setup.mjs +4 -1
  38. package/src/sso-browsers.mjs +1 -1
  39. package/src/terminal-ui.mjs +5 -3
  40. package/tools.md +15 -9
@@ -435,6 +435,190 @@ index 0000000..1ed3c2b
435
435
  + t.Errorf("got component %#v", got)
436
436
  + }
437
437
  +}
438
+ diff --git a/pkg/adt/crud.go b/pkg/adt/crud.go
439
+ index f412fb1..15ac38a 100755
440
+ --- a/pkg/adt/crud.go
441
+ +++ b/pkg/adt/crud.go
442
+ @@ -1250,7 +1250,12 @@ func (c *Client) publishUnpublishServiceBinding(ctx context.Context, action, ser
443
+ <adtcore:objectReference adtcore:name="%s"/>
444
+ </adtcore:objectReferences>`, serviceName)
445
+
446
+ - path := fmt.Sprintf("/sap/bc/adt/businessservices/odatav2/%s", action)
447
+ + // Publish jobs live under the binding's protocol; odatav2 alone cannot serve a V4 binding.
448
+ + protocol := "odatav2"
449
+ + if binding, err := c.GetSRVB(ctx, serviceName); err == nil && strings.EqualFold(binding.BindingVersion, "V4") {
450
+ + protocol = "odatav4"
451
+ + }
452
+ + path := fmt.Sprintf("/sap/bc/adt/businessservices/%s/%s", protocol, action)
453
+
454
+ resp, err := c.transport.Request(ctx, path, &RequestOptions{
455
+ Method: http.MethodPost,
456
+ diff --git a/pkg/adt/features.go b/pkg/adt/features.go
457
+ index 13aa4b0..8fb7376 100755
458
+ --- a/pkg/adt/features.go
459
+ +++ b/pkg/adt/features.go
460
+ @@ -3,6 +3,7 @@ package adt
461
+
462
+ import (
463
+ "context"
464
+ + "errors"
465
+ "fmt"
466
+ "io"
467
+ "net/http"
468
+ @@ -267,26 +268,11 @@ func (p *FeatureProber) probeAbapGit(ctx context.Context) (bool, string, error)
469
+ return false, "abapGit not found", nil
470
+ }
471
+
472
+ -// probeRAP checks if RAP development tools are available
473
+ +// probeRAP checks if RAP development tools are available.
474
+ +// DDLS predates RAP, so the behavior-definition collection is the signal.
475
+ func (p *FeatureProber) probeRAP(ctx context.Context) (bool, string, error) {
476
+ - // Check if DDLS endpoint exists
477
+ - resp, err := p.client.transport.Request(ctx, "/sap/bc/adt/ddic/ddl/sources", &RequestOptions{
478
+ - Method: http.MethodOptions,
479
+ - })
480
+ - if err != nil {
481
+ - // Check if it's a 404 vs connection error
482
+ - if strings.Contains(err.Error(), "404") {
483
+ - return false, "DDLS endpoint not available", nil
484
+ - }
485
+ - return false, "", err
486
+ - }
487
+ -
488
+ - // OPTIONS returning 200 or 405 means endpoint exists
489
+ - if resp.StatusCode == 200 || resp.StatusCode == 405 {
490
+ - return true, "RAP endpoints available", nil
491
+ - }
492
+ -
493
+ - return false, "RAP endpoints not responding", nil
494
+ + return p.probeADTResource(ctx, "/sap/bc/adt/bo/behaviordefinitions",
495
+ + "RAP endpoints available", "RAP behavior definition endpoint not available")
496
+ }
497
+
498
+ // probeAMDP checks if AMDP debugging is available
499
+ @@ -324,44 +310,35 @@ func (p *FeatureProber) probeAMDP(ctx context.Context) (bool, string, error) {
500
+ return false, "AMDP debugger not responding", nil
501
+ }
502
+
503
+ -// probeUI5 checks if UI5/Fiori BSP management is available
504
+ +// probeUI5 checks if the UI5/Fiori BSP repository is available
505
+ func (p *FeatureProber) probeUI5(ctx context.Context) (bool, string, error) {
506
+ - // Check if UI5 repository endpoint exists
507
+ - resp, err := p.client.transport.Request(ctx, "/sap/bc/adt/filestore/ui5-bsp", &RequestOptions{
508
+ - Method: http.MethodOptions,
509
+ - })
510
+ - if err != nil {
511
+ - if strings.Contains(err.Error(), "404") {
512
+ - return false, "UI5 BSP endpoint not available", nil
513
+ - }
514
+ - return false, "", err
515
+ - }
516
+ -
517
+ - if resp.StatusCode == 200 || resp.StatusCode == 405 {
518
+ - return true, "UI5 BSP repository available", nil
519
+ - }
520
+ -
521
+ - return false, "UI5 BSP not responding", nil
522
+ + return p.probeADTResource(ctx, "/sap/bc/adt/filestore/ui5-bsp/objects",
523
+ + "UI5 BSP repository available (read-only through ADT)", "UI5 BSP endpoint not available")
524
+ }
525
+
526
+ // probeTransport checks if CTS transport management is available
527
+ func (p *FeatureProber) probeTransport(ctx context.Context) (bool, string, error) {
528
+ - // Check if transport endpoint exists
529
+ - resp, err := p.client.transport.Request(ctx, "/sap/bc/adt/cts/transports", &RequestOptions{
530
+ - Method: http.MethodOptions,
531
+ - })
532
+ - if err != nil {
533
+ - if strings.Contains(err.Error(), "404") {
534
+ - return false, "CTS endpoint not available", nil
535
+ - }
536
+ - return false, "", err
537
+ - }
538
+ + return p.probeADTResource(ctx, "/sap/bc/adt/cts/transportrequests",
539
+ + "CTS transport management available", "CTS endpoint not available")
540
+ +}
541
+
542
+ - if resp.StatusCode == 200 || resp.StatusCode == 405 {
543
+ - return true, "CTS transport management available", nil
544
+ +// probeADTResource reports whether the ADT resource at path is registered.
545
+ +// ADT answers 404 only for an unknown path; a registered resource refuses OPTIONS with 400 or 405.
546
+ +func (p *FeatureProber) probeADTResource(ctx context.Context, path, available, missing string) (bool, string, error) {
547
+ + _, err := p.client.transport.Request(ctx, path, &RequestOptions{Method: http.MethodOptions})
548
+ + if err == nil {
549
+ + return true, available, nil
550
+ }
551
+ -
552
+ - return false, "CTS not responding", nil
553
+ + var apiErr *APIError
554
+ + if errors.As(err, &apiErr) {
555
+ + switch apiErr.StatusCode {
556
+ + case http.StatusBadRequest, http.StatusMethodNotAllowed:
557
+ + return true, available, nil
558
+ + case http.StatusNotFound:
559
+ + return false, missing, nil
560
+ + }
561
+ + }
562
+ + return false, "", err
563
+ }
564
+
565
+ // FeatureSummary returns a human-readable summary of all features
566
+ diff --git a/pkg/adt/features_test.go b/pkg/adt/features_test.go
567
+ new file mode 100644
568
+ index 0000000..5e880bd
569
+ --- /dev/null
570
+ +++ b/pkg/adt/features_test.go
571
+ @@ -0,0 +1,50 @@
572
+ +package adt
573
+ +
574
+ +import (
575
+ + "context"
576
+ + "io"
577
+ + "net/http"
578
+ + "net/http/httptest"
579
+ + "strings"
580
+ + "testing"
581
+ +)
582
+ +
583
+ +// Measured on S/4HANA 2023 (7.58): OPTIONS on a registered ADT resource answers
584
+ +// 400 "HTTP method OPTIONS not supported"; an unknown path answers 404.
585
+ +func TestFeatureProbesReadADTResourceAnswers(t *testing.T) {
586
+ + tests := []struct {
587
+ + name string
588
+ + feature FeatureID
589
+ + path string
590
+ + status int
591
+ + available bool
592
+ + message string
593
+ + }{
594
+ + {"RAP registered", FeatureRAP, "/sap/bc/adt/bo/behaviordefinitions", http.StatusBadRequest, true, "RAP endpoints available"},
595
+ + {"RAP absent", FeatureRAP, "/sap/bc/adt/bo/behaviordefinitions", http.StatusNotFound, false, "RAP behavior definition endpoint not available"},
596
+ + {"UI5 registered", FeatureUI5, "/sap/bc/adt/filestore/ui5-bsp/objects", http.StatusMethodNotAllowed, true, "UI5 BSP repository available (read-only through ADT)"},
597
+ + {"UI5 absent", FeatureUI5, "/sap/bc/adt/filestore/ui5-bsp/objects", http.StatusNotFound, false, "UI5 BSP endpoint not available"},
598
+ + {"CTS registered", FeatureTransport, "/sap/bc/adt/cts/transportrequests", http.StatusBadRequest, true, "CTS transport management available"},
599
+ + {"CTS server error", FeatureTransport, "/sap/bc/adt/cts/transportrequests", http.StatusInternalServerError, false, "probe failed: ADT API error: status 500"},
600
+ + }
601
+ + for _, tt := range tests {
602
+ + t.Run(tt.name, func(t *testing.T) {
603
+ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
604
+ + if r.Method != http.MethodOptions || r.URL.Path != tt.path {
605
+ + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
606
+ + http.NotFound(w, r)
607
+ + return
608
+ + }
609
+ + w.WriteHeader(tt.status)
610
+ + _, _ = io.WriteString(w, "HTTP&#x20;method&#x20;OPTIONS&#x20;not&#x20;supported")
611
+ + }))
612
+ + defer server.Close()
613
+ +
614
+ + prober := NewFeatureProber(NewClient(server.URL, "user", "pass"), DefaultFeatureConfig(), false)
615
+ + got := prober.Probe(context.Background(), tt.feature)
616
+ + if got.Available != tt.available || !strings.HasPrefix(got.Message, tt.message) {
617
+ + t.Errorf("Probe(%s) = available %v, message %q; want %v, %q", tt.feature, got.Available, got.Message, tt.available, tt.message)
618
+ + }
619
+ + })
620
+ + }
621
+ +}
438
622
  diff --git a/pkg/adt/http.go b/pkg/adt/http.go
439
623
  index 0b4d9f1..3473536 100755
440
624
  --- a/pkg/adt/http.go
@@ -562,6 +746,65 @@ index 0000000..80741da
562
746
  + }
563
747
  +}
564
748
  +
749
+ diff --git a/pkg/adt/servicebinding_publish_test.go b/pkg/adt/servicebinding_publish_test.go
750
+ new file mode 100644
751
+ index 0000000..93c19f9
752
+ --- /dev/null
753
+ +++ b/pkg/adt/servicebinding_publish_test.go
754
+ @@ -0,0 +1,53 @@
755
+ +package adt
756
+ +
757
+ +import (
758
+ + "context"
759
+ + "fmt"
760
+ + "io"
761
+ + "net/http"
762
+ + "net/http/httptest"
763
+ + "strings"
764
+ + "testing"
765
+ +)
766
+ +
767
+ +func TestPublishServiceBindingUsesTheBindingProtocol(t *testing.T) {
768
+ + tests := []struct {
769
+ + name string
770
+ + status int
771
+ + version string
772
+ + want string
773
+ + }{
774
+ + {"V4 binding", http.StatusOK, "V4", "/sap/bc/adt/businessservices/odatav4/publishjobs"},
775
+ + {"V2 binding", http.StatusOK, "V2", "/sap/bc/adt/businessservices/odatav2/publishjobs"},
776
+ + {"unreadable binding", http.StatusNotFound, "", "/sap/bc/adt/businessservices/odatav2/publishjobs"},
777
+ + }
778
+ + for _, tt := range tests {
779
+ + t.Run(tt.name, func(t *testing.T) {
780
+ + var published string
781
+ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
782
+ + switch {
783
+ + case strings.Contains(r.URL.Path, "/discovery"):
784
+ + w.Header().Set("X-CSRF-Token", "TOKEN")
785
+ + case r.Method == http.MethodGet && r.URL.Path == "/sap/bc/adt/businessservices/bindings/ZUI_DEMO":
786
+ + w.WriteHeader(tt.status)
787
+ + fmt.Fprintf(w, `<srvb:serviceBinding xmlns:srvb="http://www.sap.com/adt/ddic/ServiceBindings" xmlns:adtcore="http://www.sap.com/adt/core" adtcore:name="ZUI_DEMO"><srvb:binding srvb:type="ODATA" srvb:version="%s" srvb:category="0"/></srvb:serviceBinding>`, tt.version)
788
+ + case r.Method == http.MethodPost && strings.HasSuffix(r.URL.Path, "/publishjobs"):
789
+ + published = r.URL.Path
790
+ + _, _ = io.WriteString(w, `<asx:abap xmlns:asx="http://www.sap.com/abapxml"><asx:values><DATA><SEVERITY>OK</SEVERITY><SHORT_TEXT>Published</SHORT_TEXT></DATA></asx:values></asx:abap>`)
791
+ + default:
792
+ + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
793
+ + http.NotFound(w, r)
794
+ + }
795
+ + }))
796
+ + defer server.Close()
797
+ +
798
+ + result, err := NewClient(server.URL, "user", "pass").PublishServiceBinding(context.Background(), "ZUI_DEMO", "0001")
799
+ + if err != nil {
800
+ + t.Fatalf("PublishServiceBinding: %v", err)
801
+ + }
802
+ + if published != tt.want || result.Severity != "OK" {
803
+ + t.Errorf("published to %q with severity %q; want %q with OK", published, result.Severity, tt.want)
804
+ + }
805
+ + })
806
+ + }
807
+ +}
565
808
  diff --git a/pkg/adt/sso.go b/pkg/adt/sso.go
566
809
  index d67c4e8..a03f740 100644
567
810
  --- a/pkg/adt/sso.go
@@ -791,3 +1034,102 @@ index 8e5bed4..544b873 100644
791
1034
  Headless: !headed,
792
1035
  Timeout: p.timeout(headed),
793
1036
  Insecure: p.cfg.Insecure,
1037
+ diff --git a/pkg/adt/ui5.go b/pkg/adt/ui5.go
1038
+ index 6d57bb3..425f1b1 100755
1039
+ --- a/pkg/adt/ui5.go
1040
+ +++ b/pkg/adt/ui5.go
1041
+ @@ -6,6 +6,7 @@ import (
1042
+ "fmt"
1043
+ "net/http"
1044
+ "net/url"
1045
+ + "regexp"
1046
+ "strings"
1047
+ )
1048
+
1049
+ @@ -117,9 +118,19 @@ func (c *Client) UI5ListApps(ctx context.Context, query string, maxResults int)
1050
+ return nil, fmt.Errorf("parsing UI5 apps list: %w", err)
1051
+ }
1052
+
1053
+ - // Convert Atom entries to UI5App
1054
+ - apps := make([]UI5App, 0, len(feed.Entries))
1055
+ + // The ADT filestore ignores name and maxResults (S/4HANA 2023 returned all 2797 apps), so apply both here.
1056
+ + var match *regexp.Regexp
1057
+ + if query != "" {
1058
+ + match = regexp.MustCompile("(?i)^" + strings.ReplaceAll(regexp.QuoteMeta(query), `\*`, ".*") + "$")
1059
+ + }
1060
+ + apps := make([]UI5App, 0, min(len(feed.Entries), maxResults))
1061
+ for _, entry := range feed.Entries {
1062
+ + if len(apps) == maxResults {
1063
+ + break
1064
+ + }
1065
+ + if match != nil && !match.MatchString(entry.Title) {
1066
+ + continue
1067
+ + }
1068
+ apps = append(apps, UI5App{
1069
+ Name: entry.Title,
1070
+ Description: entry.Summary,
1071
+ diff --git a/pkg/adt/ui5_list_filter_test.go b/pkg/adt/ui5_list_filter_test.go
1072
+ new file mode 100644
1073
+ index 0000000..1385d6d
1074
+ --- /dev/null
1075
+ +++ b/pkg/adt/ui5_list_filter_test.go
1076
+ @@ -0,0 +1,59 @@
1077
+ +package adt
1078
+ +
1079
+ +import (
1080
+ + "context"
1081
+ + "io"
1082
+ + "net/http"
1083
+ + "net/http/httptest"
1084
+ + "testing"
1085
+ +)
1086
+ +
1087
+ +// SAP's ADT filestore answers with every app whatever name and maxResults say.
1088
+ +func TestUI5ListAppsFiltersWhatTheFilestoreIgnores(t *testing.T) {
1089
+ + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1090
+ + if r.URL.Path != "/sap/bc/adt/filestore/ui5-bsp/objects" {
1091
+ + t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
1092
+ + http.NotFound(w, r)
1093
+ + return
1094
+ + }
1095
+ + w.Header().Set("Content-Type", "application/atom+xml")
1096
+ + _, _ = io.WriteString(w, `<atom:feed xmlns:atom="http://www.w3.org/2005/Atom">`+
1097
+ + `<atom:entry><atom:id>ZAPP_ONE</atom:id><atom:title>ZAPP_ONE</atom:title><atom:summary>One</atom:summary></atom:entry>`+
1098
+ + `<atom:entry><atom:id>/UI2/LAUNCHPAD</atom:id><atom:title>/UI2/LAUNCHPAD</atom:title></atom:entry>`+
1099
+ + `<atom:entry><atom:id>ZAPP_TWO</atom:id><atom:title>ZAPP_TWO</atom:title></atom:entry>`+
1100
+ + `</atom:feed>`)
1101
+ + }))
1102
+ + defer server.Close()
1103
+ + client := NewClient(server.URL, "user", "pass")
1104
+ +
1105
+ + tests := []struct {
1106
+ + query string
1107
+ + maxResults int
1108
+ + want []string
1109
+ + }{
1110
+ + {"z*", 1, []string{"ZAPP_ONE"}},
1111
+ + {"Z*", 5, []string{"ZAPP_ONE", "ZAPP_TWO"}},
1112
+ + {"*LAUNCH*", 5, []string{"/UI2/LAUNCHPAD"}},
1113
+ + {"", 0, []string{"ZAPP_ONE", "/UI2/LAUNCHPAD", "ZAPP_TWO"}},
1114
+ + }
1115
+ + for _, tt := range tests {
1116
+ + apps, err := client.UI5ListApps(context.Background(), tt.query, tt.maxResults)
1117
+ + if err != nil {
1118
+ + t.Fatalf("UI5ListApps(%q, %d): %v", tt.query, tt.maxResults, err)
1119
+ + }
1120
+ + var got []string
1121
+ + for _, app := range apps {
1122
+ + got = append(got, app.Name)
1123
+ + }
1124
+ + if len(got) != len(tt.want) {
1125
+ + t.Errorf("UI5ListApps(%q, %d) = %v, want %v", tt.query, tt.maxResults, got, tt.want)
1126
+ + continue
1127
+ + }
1128
+ + for i := range got {
1129
+ + if got[i] != tt.want[i] {
1130
+ + t.Errorf("UI5ListApps(%q, %d) = %v, want %v", tt.query, tt.maxResults, got, tt.want)
1131
+ + break
1132
+ + }
1133
+ + }
1134
+ + }
1135
+ +}
@@ -47,7 +47,7 @@ async function main() {
47
47
  checksums.push(`${createHash('sha256').update(bytes).digest('hex')} ${output.slice(dist.length + 1)}`);
48
48
  }
49
49
  await writeFile(join(dist, 'checksums.txt'), `${checksums.join('\n')}\n`);
50
- console.error(`Built ${targets.length} patched VSP binaries at ${dist}`);
50
+ console.error(`Built ${targets.length} runtime executables at ${dist}`);
51
51
  }
52
52
 
53
- main().catch(error => { console.error(`build-vsp: ${error.message}`); process.exitCode = 1; });
53
+ main().catch(error => { console.error(`runtime build: ${error.message}`); process.exitCode = 1; });
@@ -1,5 +1,6 @@
1
1
  import { open, readFile } from 'node:fs/promises';
2
2
  import { closeSync, openSync, realpathSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { dirname, join, resolve } from 'node:path';
4
5
  import { fileURLToPath } from 'node:url';
5
6
  import { installBinary } from '../src/binary.mjs';
@@ -41,11 +42,21 @@ async function announce(message, tone = 'info') {
41
42
 
42
43
  function openControllingTerminal() {
43
44
  if (process.platform === 'win32') {
44
- // fs cannot open CONIN$/CONOUT$ (libuv path normalization rejects the
45
- // device names), so there is no controlling-terminal recovery on
46
- // Windows: npm-script installs run non-interactively and the printed
47
- // guidance points at `sap-ai-dev --setup` in a real terminal.
48
- throw new Error('controlling terminal recovery is unavailable on Windows');
45
+ // npm can pipe lifecycle script streams even when the install was
46
+ // launched from an interactive Windows console. Verify console access,
47
+ // then attach Node TTY streams to its standard console devices.
48
+ const probe = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', '[Console]::OpenStandardInput(); [Console]::OpenStandardOutput()'], { stdio: 'ignore', windowsHide: true });
49
+ if (probe.error || probe.status !== 0) throw new Error('interactive console is unavailable');
50
+ const inputFd = openSync('CONIN$', 'r');
51
+ let outputFd;
52
+ try {
53
+ outputFd = openSync('CONOUT$', 'w');
54
+ return { input: new TTYReadStream(inputFd), output: new TTYWriteStream(outputFd) };
55
+ } catch (error) {
56
+ closeSync(inputFd);
57
+ if (outputFd !== undefined) closeSync(outputFd);
58
+ throw error;
59
+ }
49
60
  }
50
61
  const inputFd = openSync('/dev/tty', 'r');
51
62
  let outputFd;
@@ -299,8 +310,8 @@ async function main() {
299
310
  if (process.env.npm_config_ignore_scripts === 'true') return;
300
311
 
301
312
  // Go is intentionally NOT provisioned here: the installed package never
302
- // runs `go` (only the repository-only build:vsp script does), so a toolchain
303
- // download at install time was pure cost. The pinned, checksum-verified VSP
313
+ // runs `go` (only the repository-only build script does), so a toolchain
314
+ // download at install time was pure cost. The pinned, checksum-verified runtime
304
315
  // binary ships with the package; the download below is the fallback.
305
316
  let binary = brandedEnvValue(runtimeEnv, 'BINARY');
306
317
  try {
@@ -309,19 +320,19 @@ async function main() {
309
320
  } else {
310
321
  // The bundled binary resolves in milliseconds; the animation only
311
322
  // appears when provisioning actually takes time (download fallback).
312
- const stopBinaryProgress = startProgress('Preparing the pinned VSP runtime for this platform', { output: process.stderr, env: runtimeEnv });
323
+ const stopBinaryProgress = startProgress('Preparing the pinned runtime for this platform', { output: process.stderr, env: runtimeEnv });
313
324
  try {
314
325
  binary = await installBinary(pkg, { env: runtimeEnv });
315
326
  stopBinaryProgress();
316
- await announce('Pinned VSP binary installed and ready.', 'success');
327
+ await announce('Pinned runtime installed and ready.', 'success');
317
328
  } catch (error) {
318
329
  stopBinaryProgress();
319
- throw new Error(`VSP binary provisioning failed: ${error.message}`);
330
+ throw new Error(`Runtime provisioning failed: ${error.message}`);
320
331
  }
321
332
  }
322
333
  } catch (error) {
323
334
  await announce(error.message, 'error');
324
- await announce('Set SAP_AI_DEV_TOOLKIT_BINARY only when supplying a trusted prebuilt VSP executable.', 'info');
335
+ await announce('Set SAP_AI_DEV_TOOLKIT_BINARY only when supplying a trusted prebuilt runtime executable.', 'info');
325
336
  process.exitCode = 1;
326
337
  return;
327
338
  }
@@ -60,7 +60,7 @@ function runNpm(npmArgs, env) {
60
60
  }
61
61
 
62
62
  // dist/ is gitignored, so a publish from a clean checkout silently ships no
63
- // VSP binaries and every `npm install` of it fails (the 0.7.0 incident):
63
+ // runtime executables and every `npm install` of it fails (the 0.7.0 incident):
64
64
  // postinstall only downloads from a GitHub release as a fallback. The
65
65
  // prepublishOnly hook in package.json builds the binaries and re-invokes this
66
66
  // script with --verify-only, so an incomplete dist/ can never be published —
@@ -84,12 +84,12 @@ async function main() {
84
84
  const patch = args.has('--patch');
85
85
  if (dryRun && patch) throw new Error('--dry-run cannot be combined with --patch; it never changes the package version');
86
86
 
87
- // Second half of the prepublishOnly hook (after build-vsp.mjs ran): refuse
87
+ // Second half of the prepublishOnly hook (after the build script ran): refuse
88
88
  // to let an incomplete dist/ reach the registry.
89
89
  if (args.has('--verify-only')) {
90
90
  const missing = await missingDistBinaries();
91
- if (missing.length > 0) throw new Error(`dist/checksums.txt is missing ${missing.join(', ')}; run npm run build:vsp first`);
92
- console.error('dist/checksums.txt lists every pinned VSP binary.');
91
+ if (missing.length > 0) throw new Error(`dist/checksums.txt is missing ${missing.join(', ')}; run the package build script first`);
92
+ console.error('dist/checksums.txt lists every pinned runtime executable.');
93
93
  return;
94
94
  }
95
95
 
package/src/binary.mjs CHANGED
@@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url';
6
6
  import { brandedEnvValue } from './branding.mjs';
7
7
 
8
8
  const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
9
- // Single source of truth for the upstream VSP commit; scripts/build-vsp.mjs
9
+ // Single source of truth for the pinned upstream commit; scripts/build-vsp.mjs
10
10
  // imports this constant so the built artifact and the runtime cannot drift.
11
11
  export const UPSTREAM_COMMIT = '9886d2727f47506368b0a3c2f1c1766f1200f747';
12
12
 
@@ -20,11 +20,11 @@ const platformMap = {
20
20
 
21
21
  export function binaryTarget(platform = process.platform, arch = process.arch) {
22
22
  const target = platformMap[`${platform}:${arch}`];
23
- if (!target) throw new Error(`Unsupported platform ${platform}/${arch}. Set SAP_AI_DEV_TOOLKIT_BINARY or build cmd/vsp with scripts/build-vsp.mjs.`);
23
+ if (!target) throw new Error(`Unsupported platform ${platform}/${arch}. Set SAP_AI_DEV_TOOLKIT_BINARY or run the package build script.`);
24
24
  return { os: target[0], arch: target[1], extension: target[2], asset: `vsp-${target[0]}-${target[1]}${target[2]}` };
25
25
  }
26
26
 
27
- // Every VSP asset the npm tarball must bundle; publish:npm refuses to ship a
27
+ // Every runtime asset the npm tarball must bundle; publish:npm refuses to ship a
28
28
  // package whose dist/checksums.txt does not list all of them.
29
29
  export function packagedBinaryAssets() {
30
30
  return Object.keys(platformMap).map(key => binaryTarget(...key.split(':')).asset);
@@ -76,7 +76,7 @@ async function verifyAgainstPackagedChecksums(path, target) {
76
76
 
77
77
  export function packageRepositoryUrl(pkg) {
78
78
  const raw = typeof pkg.repository === 'string' ? pkg.repository : pkg.repository?.url;
79
- if (!raw) throw new Error('package.json repository.url is required to resolve VSP release assets');
79
+ if (!raw) throw new Error('package.json repository.url is required to resolve runtime release assets');
80
80
  return raw.replace(/^git\+/, '').replace(/\.git$/, '').replace(/\/$/, '');
81
81
  }
82
82
 
@@ -114,13 +114,13 @@ export async function findBinary(pkg, options = {}) {
114
114
  await stat(bundled);
115
115
  const verified = await verifyAgainstPackagedChecksums(bundled, target);
116
116
  if (verified === false) {
117
- log(`bundled VSP binary ${bundled} failed checksum verification; ignoring it`);
117
+ log(`bundled runtime executable ${bundled} failed checksum verification; ignoring it`);
118
118
  } else {
119
- if (verified === null) log(`bundled VSP binary ${target.asset} has no packaged checksum entry; using it unverified`);
119
+ if (verified === null) log(`bundled runtime executable ${target.asset} has no packaged checksum entry; using it unverified`);
120
120
  return bundled;
121
121
  }
122
122
  } catch (error) {
123
- log(`bundled VSP binary not found at ${bundled} (${error.code || error.message})`);
123
+ log(`bundled runtime executable not found at ${bundled} (${error.code || error.message})`);
124
124
  }
125
125
  }
126
126
  const path = cachedBinaryPath(pkg, options.platform, options.arch, env);
@@ -128,9 +128,9 @@ export async function findBinary(pkg, options = {}) {
128
128
  await stat(path);
129
129
  const verified = await verifyAgainstPackagedChecksums(path, target);
130
130
  if (verified === false) {
131
- log(`cached VSP binary ${target.asset} failed checksum verification; ignoring it`);
131
+ log(`cached runtime executable ${target.asset} failed checksum verification; ignoring it`);
132
132
  } else {
133
- if (verified === null) log(`cached VSP binary ${target.asset} has no packaged checksum entry; using it unverified`);
133
+ if (verified === null) log(`cached runtime executable ${target.asset} has no packaged checksum entry; using it unverified`);
134
134
  return path;
135
135
  }
136
136
  } catch {}
@@ -141,7 +141,7 @@ export async function findBinary(pkg, options = {}) {
141
141
  await stat(legacyPath);
142
142
  const verified = await verifyAgainstPackagedChecksums(legacyPath, target);
143
143
  if (verified === true) return legacyPath;
144
- log(`legacy cached VSP binary ${legacyPath} has no packaged checksum match; ignoring it`);
144
+ log(`legacy cached runtime executable ${legacyPath} has no packaged checksum match; ignoring it`);
145
145
  } catch {}
146
146
  return null;
147
147
  }
@@ -155,7 +155,7 @@ export async function installBinary(pkg, options = {}) {
155
155
  await stat(bundled);
156
156
  const target0 = binaryTarget(options.platform, options.arch);
157
157
  if (await verifyAgainstPackagedChecksums(bundled, target0) !== false) return bundled;
158
- throw new Error(`bundled VSP binary ${target0.asset} failed checksum verification`);
158
+ throw new Error(`bundled runtime executable ${target0.asset} failed checksum verification`);
159
159
  } catch (error) {
160
160
  if (String(error?.message || '').includes('checksum verification')) throw error;
161
161
  }
@@ -169,7 +169,7 @@ export async function downloadBinary(pkg, options = {}) {
169
169
  const base = releaseBaseUrl(pkg, pkg.version);
170
170
  const assetUrl = brandedEnvValue(env, 'BINARY_URL') || `${base}/${target.asset}`;
171
171
  const response = await fetch(assetUrl);
172
- if (!response.ok) throw new Error(`VSP binary download failed (${response.status}); set SAP_AI_DEV_TOOLKIT_BINARY or SAP_AI_DEV_TOOLKIT_BINARY_URL`);
172
+ if (!response.ok) throw new Error(`Runtime binary download failed (${response.status}); set SAP_AI_DEV_TOOLKIT_BINARY or SAP_AI_DEV_TOOLKIT_BINARY_URL`);
173
173
  const bytes = Buffer.from(await response.arrayBuffer());
174
174
  const actual = createHash('sha256').update(bytes).digest('hex');
175
175
  // Prefer the packaged checksum manifest (immutable per published version)
@@ -180,13 +180,13 @@ export async function downloadBinary(pkg, options = {}) {
180
180
  if (!expected) {
181
181
  const checksumUrl = `${base}/checksums.txt`;
182
182
  const checksumResponse = await fetch(checksumUrl);
183
- if (!checksumResponse.ok) throw new Error(`VSP checksum download failed (${checksumResponse.status}); set SAP_AI_DEV_TOOLKIT_BINARY or SAP_AI_DEV_TOOLKIT_BINARY_URL`);
183
+ if (!checksumResponse.ok) throw new Error(`Runtime checksum download failed (${checksumResponse.status}); set SAP_AI_DEV_TOOLKIT_BINARY or SAP_AI_DEV_TOOLKIT_BINARY_URL`);
184
184
  const checksumText = await checksumResponse.text();
185
185
  expected = checksumText.split(/\r?\n/).map(line => line.trim().split(/\s+/)).find(parts => parts.length >= 2 && (parts[1] === target.asset || parts[1].endsWith(`/${target.asset}`)))?.[0]?.toLowerCase();
186
186
  expectedSource = 'release checksums.txt';
187
187
  if (!expected) throw new Error(`checksums.txt has no entry for ${target.asset}; set SAP_AI_DEV_TOOLKIT_BINARY or SAP_AI_DEV_TOOLKIT_BINARY_URL`);
188
188
  }
189
- if (actual !== expected) throw new Error(`VSP checksum mismatch for ${target.asset} (verified against ${expectedSource})`);
189
+ if (actual !== expected) throw new Error(`Runtime checksum mismatch for ${target.asset} (verified against ${expectedSource})`);
190
190
  const dir = cacheDirectory(env);
191
191
  await mkdir(dir, { recursive: true, mode: 0o700 });
192
192
  await chmod(dir, 0o700);