sap-ai-dev-toolkit 0.9.9906 → 0.9.9909
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/.github/agents/abap-developer.agent.md +2 -0
- package/.github/agents/abap-runtime-debugger.agent.md +2 -0
- package/.github/agents/rap-service-developer.agent.md +2 -0
- package/.github/agents/sap-solution-architect.agent.md +2 -0
- package/.github/skills/abap-debugging/SKILL.md +2 -0
- package/.github/skills/abap-development/SKILL.md +2 -0
- package/.github/skills/abap-runtime-analysis/SKILL.md +2 -0
- package/.github/skills/abap-testing-quality/SKILL.md +2 -0
- package/.github/skills/cds-development/SKILL.md +2 -0
- package/.github/skills/clean-core-extensibility/SKILL.md +2 -0
- package/.github/skills/rap-development/SKILL.md +2 -0
- package/.github/skills/rap-service-delivery/SKILL.md +2 -0
- package/.github/skills/sap-sdlc-orchestration/SKILL.md +2 -0
- package/.github/skills/sap-standard-api-analysis/SKILL.md +2 -0
- package/.github/skills/sap-transport-release/SKILL.md +2 -0
- package/README.md +7 -3
- package/dist/checksums.txt +5 -5
- package/dist/vsp-darwin-arm64 +0 -0
- package/dist/vsp-darwin-x64 +0 -0
- package/dist/vsp-linux-arm64 +0 -0
- package/dist/vsp-linux-x64 +0 -0
- package/dist/vsp-win32-x64.exe +0 -0
- package/package.json +2 -2
- package/patches/vsp-bas-proxy-auth.patch +162 -5
- package/src/abaplint.mjs +229 -24
- package/src/harness-mcp-config.mjs +9 -1
- package/src/harnesses.mjs +21 -4
- package/src/launcher.mjs +1 -1
- package/src/mcp-proxy.mjs +7 -1
- package/src/setup.mjs +54 -10
- package/src/sso-browsers.mjs +154 -4
- package/tools.md +3 -2
|
@@ -5,6 +5,8 @@ target: vscode
|
|
|
5
5
|
user-invocable: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
You are an ABAP development agent for SAP Business Application Studio (BAS). Follow this workflow in order. Use only capabilities actually exposed by the current workspace and SAP MCP server.
|
|
9
11
|
|
|
10
12
|
**Direct MCP invocation is mandatory.** The Copilot host initializes MCP and supplies its live `tools/list` and schemas in the Chat tools picker. Invoke the attached destination tools directly. Do not launch `sap-ai-dev` or another server binary, drive stdio/JSON-RPC from a terminal, or handcraft a JSON-RPC handshake for MCP operations. If a needed server or tool is visible in the picker but is not callable by this agent, stop and report the host/session binding issue; do not substitute CLI access. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ target: vscode
|
|
|
5
5
|
user-invocable: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
You are a specialized ABAP runtime debugging and performance diagnosis agent for SAP Business Application Studio (BAS). Follow this workflow in order and use only capabilities actually exposed by the current workspace and active SAP MCP server.
|
|
9
11
|
|
|
10
12
|
**Direct MCP invocation is mandatory.** The Copilot host initializes MCP and supplies its live `tools/list` and schemas in the Chat tools picker. Invoke the attached destination tools directly. Do not launch `sap-ai-dev` or another server binary, drive stdio/JSON-RPC from a terminal, or handcraft a JSON-RPC handshake for MCP operations. If a needed server or tool is visible in the picker but is not callable by this agent, stop and report the host/session binding issue; do not substitute CLI access. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ target: vscode
|
|
|
5
5
|
user-invocable: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP/RAP/CDS sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
You are a specialized SAP RAP service development agent for SAP Business Application Studio (BAS). Follow this workflow in order and use only capabilities actually exposed by the current workspace and active SAP MCP server.
|
|
9
11
|
|
|
10
12
|
**Direct MCP invocation is mandatory.** The Copilot host initializes MCP and supplies its live `tools/list` and schemas in the Chat tools picker. Invoke the attached destination tools directly. Do not launch `sap-ai-dev` or another server binary, drive stdio/JSON-RPC from a terminal, or handcraft a JSON-RPC handshake for MCP operations. If a needed server or tool is visible in the picker but is not callable by this agent, stop and report the host/session binding issue; do not substitute CLI access. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ target: vscode
|
|
|
5
5
|
user-invocable: true
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP/RAP/CDS sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
You are an SAP solution architecture and SDLC orchestration agent for SAP Business Application Studio (BAS). Your job is to investigate the target SAP system, decide how a requested business requirement should be solved, recommend SAP standard capabilities and released APIs, design a Clean Core solution, and drive the delivery lifecycle through implementation-agent handoffs and validation gates. Prefer SAP standard capabilities, released APIs, Clean Core compliant extension points, and side-by-side extensibility over custom core changes.
|
|
9
11
|
|
|
10
12
|
Follow this workflow in order and use only capabilities actually exposed by the current workspace and active SAP MCP server.
|
|
@@ -5,6 +5,8 @@ description: Diagnose ABAP runtime failures using application logs, source/refer
|
|
|
5
5
|
|
|
6
6
|
# ABAP debugging
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once; use the exact live tool name and schema for the target. Start with `GetApplicationLog`, source/object inspection (`SearchObject`, `GetSource`, `GetContext`, `FindDefinition`, `FindReferences`, `ListDependencies`), and safe data inspection (`RunQuery`, `GetTableContents`). For an authorized debugger session, use `SetBreakpoint`, review registered breakpoints with `GetBreakpoints`, attach with `DebuggerAttach`, inspect with `DebuggerGetStack` / `DebuggerGetVariables`, step with `DebuggerStep`, and finish with `DebuggerDetach`. Avoid attaching to another user's session. Dump, trace, debugger-listen, delete-breakpoint, and arbitrary execution/RFC tools are not exposed by this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Implement or change ABAP programs, classes, interfaces, function gr
|
|
|
5
5
|
|
|
6
6
|
# ABAP development
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Locate objects and usages with `SearchObject`, `GrepObjects`, and `GrepPackages`; read with `GetSource` and `GetContext`; trace callers or dependencies with `FindDefinition` and `FindReferences`; compare workspace and SAP versions with `CompareSource`. Use `EditSource` for a localized change and `WriteSource` for a larger rewrite. Format with `PrettyPrint`; lint caller-supplied source with `LintABAP`; validate with `SyntaxCheck` and `RunUnitTests`, plus `RunATCCheck` when exposed and relevant. For Clean Core requirements, use `GetAPIReleaseState` on each relevant dependency. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Analyze ABAP runtime incidents, debugger state, and performance sym
|
|
|
5
5
|
|
|
6
6
|
# ABAP runtime analysis
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once and use the exact live names and schemas. Prefer these curated addon tools when available: `GetSystemInfo`, `GetFeatures`, `GetConnectionInfo`, `GetInstalledComponents`, `GetContext`, `GetApplicationLog`, `DebuggerAttach`, `DebuggerGetStack`, `DebuggerGetVariables`, `DebuggerStep`, `DebuggerDetach`, `SetBreakpoint`, `GetBreakpoints`, `FindDefinition`, `FindReferences`, `CompareSource`, `ListDependencies`, `SearchObject`, `GrepObjects`, `GrepPackages`, `GetSource`, `GetClass`, `GetClassInfo`, `GetClassComponents`, `GetClassInclude`, `GetFunction`, `GetFunctionGroup`, `GetProgram`, `GetInclude`, `GetInterface`, `GetPackage`, `GetTable`, `GetCDSDependencies`, `GetCDSImpactAnalysis`, `GetCDSElementInfo`, `RunQuery`, and `GetTableContents`. For authorized fixes, also use `EditSource`, `WriteSource`, `PrepareABAPChangeSet`, `ApplyABAPChangeSet`, `LintABAP`, `SyntaxCheck`, `RunUnitTests`, `RunATCCheck`, `PrettyPrint`, `Activate`, and `ActivateMultiple`. Dump, trace, arbitrary execution/RFC, and delete-breakpoint helpers are not exposed by this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Design ABAP Unit behavior tests and validate ABAP changes with lint
|
|
|
5
5
|
|
|
6
6
|
# ABAP testing and quality
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool selection
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once, then use only task-relevant tools with their returned schemas: `LintABAP` for caller-supplied source, `SyntaxCheck` for SAP syntax validation, `RunUnitTests` for ABAP Unit, and `RunATCCheck` for ATC. Unavailable tools are not passes. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Implement, change, or analyze ABAP CDS data definitions and their d
|
|
|
5
5
|
|
|
6
6
|
# CDS development
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized CDS/ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available for ABAP sources. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Locate views and usages with `SearchObject` and `GrepObjects`; read DDLS with `GetSource`; compare workspace and SAP versions with `CompareSource`; use `GetCDSDependencies` for upstream dependencies, `GetCDSImpactAnalysis` for consumers, and `GetCDSElementInfo` for element metadata. Trace references with `FindReferences`; edit via `EditSource` / `WriteSource`. Validate with `SyntaxCheck`, `RunATCCheck`, activation via `Activate` / `ActivateMultiple`, and read-only runtime queries via `RunQuery` or `GetTableContents` when exposed. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Design SAP extensions using Clean Core principles, released extensi
|
|
|
5
5
|
|
|
6
6
|
# Clean Core extensibility
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever the selected extension edits or hands off serialized ABAP/RAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Inspect system capabilities with `GetSystemInfo`, `GetFeatures`, and `GetInstalledComponents`; inspect source and dependencies with `GetSource`, `GetContext`, `FindDefinition`, `FindReferences`, `CompareSource`, `ListDependencies`, `GetCDSDependencies`, and `GetCDSImpactAnalysis`. Check release status with `GetAPIReleaseState` or batch with `PlanABAPCloudMigration` where exposed. Use transport tools only for planning/readiness context and never claim transport release/deletion support; transport release is unavailable through this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Develop SAP RAP business objects, behavior, projections, and OData
|
|
|
5
5
|
|
|
6
6
|
# RAP development
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized RAP/CDS/ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Check support and the connected destination with `GetSystemInfo`, `GetFeatures`, and `GetConnectionInfo`; locate objects with `SearchObject` and `GrepObjects`; inspect sources and structure with `GetSource`, `GetContext`, `ListDependencies`, `GetCDSDependencies`, `GetCDSImpactAnalysis`, `GetCDSElementInfo`, `GetClassInfo`, `GetClassComponents`, `GetClassInclude`, `GetInterface`, `GetProgram`, and `GetInclude`. Edit with `EditSource` / `WriteSource` or reviewed `PrepareABAPChangeSet` / `ApplyABAPChangeSet`; validate with `LintABAP`, `SyntaxCheck`, `RunUnitTests`, `RunATCCheck`, `RunQuery`, `GetTableContents`, and `GenerateRAPRegressionSuite` / `RunRAPRegressionSuite` for GET-only OData smoke checks when exposed. Activate with `Activate` / `ActivateMultiple` only when requested. Create service bindings with `CreateObject` (`SRVB/SVB`; set `binding_version` explicitly because V2 is the default) and publish or withdraw them with `PublishServiceBinding` / `UnpublishServiceBinding` only when publication is requested. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Validate and deliver SAP RAP OData services, behavior implementatio
|
|
|
5
5
|
|
|
6
6
|
# RAP service delivery
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever this workflow edits or hands off serialized RAP/CDS/ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once and use the exact live names and schemas. Prefer these curated addon tools when available: `GetSystemInfo`, `GetFeatures`, `GetConnectionInfo`, `SearchObject`, `GrepObjects`, `GrepPackages`, `GetSource`, `GetContext`, `ListDependencies`, `GetCDSDependencies`, `GetCDSImpactAnalysis`, `GetCDSElementInfo`, `GetClass`, `GetClassInfo`, `GetClassComponents`, `GetClassInclude`, `GetInterface`, `EditSource`, `WriteSource`, `PrepareABAPChangeSet`, `ApplyABAPChangeSet`, `LintABAP`, `SyntaxCheck`, `RunUnitTests`, `RunATCCheck`, `PrettyPrint`, `RunQuery`, `GetTableContents`, `GenerateRAPRegressionSuite`, `RunRAPRegressionSuite`, `GetApplicationLog`, `GetInactiveObjects`, `Activate`, `ActivateMultiple`, `CreateObject` (service bindings), `PublishServiceBinding`, and `UnpublishServiceBinding`. Read UI5 apps already deployed to the ABAP repository with `UI5ListApps`, `UI5GetApp`, and `UI5GetFileContent`. UI5 deployment, dump, trace, arbitrary execution/RFC, and code-coverage tools are not exposed by this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Orchestrate the SAP delivery lifecycle from requirement analysis th
|
|
|
5
5
|
|
|
6
6
|
# SAP SDLC orchestration
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever an implementation package edits or hands off serialized ABAP/RAP/CDS sources in a workspace, require `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Investigate system and repository context with `GetSystemInfo`, `GetFeatures`, `GetConnectionInfo`, `GetInstalledComponents`, `SearchObject`, `GrepObjects`, `GrepPackages`, `GetSource`, `GetContext`, `FindDefinition`, `FindReferences`, `CompareSource`, and dependency tools (`ListDependencies`, `GetCDSDependencies`, `GetCDSImpactAnalysis`). Validate with `LintABAP`, `SyntaxCheck`, `RunUnitTests`, `RunATCCheck`, runtime queries (`RunQuery`, `GetTableContents`), `GetApplicationLog`, and `GenerateRAPRegressionSuite` / `RunRAPRegressionSuite` when exposed. Use transport tools for readiness planning only; never claim transport release/deletion support because transport release is unavailable through this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Assess requirements against SAP standard capabilities, released API
|
|
|
5
5
|
|
|
6
6
|
# SAP standard and API analysis
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** Whenever the recommendation includes editing or handing off serialized ABAP/RAP sources in a workspace, require `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Any actual activation still requires a successful `LintABAP` run immediately before activation on the latest source for the exact activation request. Never activate merely to run tests. Skip for analysis-only work or when no serialized project source exists.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Inspect target capabilities with `GetSystemInfo`, `GetFeatures`, `GetConnectionInfo`, and `GetInstalledComponents`. Search candidates with `SearchObject`, `GrepObjects`, `GrepPackages`, `GetSource`, `GetContext`, `FindDefinition`, `FindReferences`, `ListDependencies`, `GetCDSDependencies`, `GetCDSImpactAnalysis`, and `GetCDSElementInfo`. Verify released API status with `GetAPIReleaseState` or batch with `PlanABAPCloudMigration` when exposed. Use `RunQuery` or `GetTableContents` only for safe read-only evidence. Never claim transport release/deletion support; transport release is unavailable through this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
|
@@ -5,6 +5,8 @@ description: Prepare ABAP changes for SAP transport and verify request contents,
|
|
|
5
5
|
|
|
6
6
|
# SAP transport preparation
|
|
7
7
|
|
|
8
|
+
**Workspace preflight.** When a transport request includes serialized ABAP sources in a workspace, run `LintABAPProject` (`lint_abap_project`) before sending changes to SAP; if unavailable, report the skipped preflight and use caller-supplied `LintABAP` if available. Confirm the configured root is the intended project (`SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT`, or the MCP process working directory by default; `directory` is relative to that root), `targetRelease` matches the SAP target, `dependencies.missingFolders` and `syntaxRules.disabled` are empty, and `status` is `clean`. `issues`, `incomplete`, `no-abap-files`, or a mismatched root/release are not passes. This is static analysis only and does not validate SAP activation or runtime; caller-supplied `LintABAP` on the latest source for the exact object set remains mandatory immediately before activation. Skip when no local serialized sources exist.
|
|
9
|
+
|
|
8
10
|
## Tool shortlist
|
|
9
11
|
|
|
10
12
|
Query the active MCP server's live `tools/list` once; use task-relevant tools with their returned schemas. Identify candidates with `GetUserTransports` or `ListTransports`; inspect a request with `GetTransport` / `GetTransportInfo`; use `CheckTransportReadiness` to collect transport, dependency, inactive-object, ABAP Unit, and ATC evidence when available. Activate with `Activate` / `ActivateMultiple` only when requested. `CreateTransport` is state-changing and requires explicit authorization. Release and deletion are not available through this addon. Tool names shown in PascalCase (such as `GetSource` or `LintABAP`) are logical names; the live MCP surface exposes them as lowercase snake_case. A server scoped to a single destination (the normal case for generated `mcp.json` entries) exposes them unprefixed, so `GetSource` appears as `get_source` and `LintABAP` as `lint_abap`; when several destinations share one server, each name is destination-prefixed with its slug for disambiguation (for example `cfd_run_query`). Always call the exact names returned by `tools/list`.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
17
|
<img src="https://img.shields.io/badge/SAP-Business%20Application%20Studio-0A6ED1?style=flat-square&logo=sap&logoColor=white" alt="SAP Business Application Studio">
|
|
18
|
-
<img src="https://img.shields.io/badge/AI%20agents-Copilot%20%7C%20Claude%20%7C%20Codex%20%7C%20Cursor%20%7C%20Gemini%20%7C%20opencode%20%7C%20Pi-000000?style=flat-square" alt="Works with GitHub Copilot, Claude Code, OpenAI Codex, Cursor, Gemini CLI, opencode,
|
|
18
|
+
<img src="https://img.shields.io/badge/AI%20agents-Copilot%20%7C%20Claude%20%7C%20Codex%20%7C%20Cursor%20%7C%20Gemini%20%7C%20opencode%20%7C%20Pi%20%7C%20OMP%20%7C%20Cline%20%7C%20Roo%20%7C%20Windsurf-000000?style=flat-square" alt="Works with GitHub Copilot, Claude Code, OpenAI Codex, Cursor, Gemini CLI, opencode, Pi Coding Agent, Oh My Pi, Cline, Roo Code, and Windsurf">
|
|
19
19
|
<img src="https://img.shields.io/badge/MCP-enabled-7B61FF?style=flat-square" alt="MCP enabled">
|
|
20
20
|
<img src="https://img.shields.io/badge/SAP%20ADT-connected-0A6ED1?style=flat-square&logo=sap&logoColor=white" alt="SAP ADT connected">
|
|
21
21
|
<img src="https://img.shields.io/badge/ABAP-CDS%20%7C%20RAP-EA4AAA?style=flat-square" alt="ABAP CDS RAP">
|
|
@@ -89,7 +89,7 @@ Use the attached destination-prefixed tools directly in chat. Do not launch `sap
|
|
|
89
89
|
|
|
90
90
|
**Included skills:** `abap-development` · `abap-testing-quality` · `cds-development` · `rap-development` · `abap-debugging` · `abap-runtime-analysis` · `rap-service-delivery` · `sap-standard-api-analysis` · `clean-core-extensibility` · `sap-sdlc-orchestration` · `sap-transport-release` · `hana-cloud-inspection` · `hana-cloud-native-development` · `hana-cloud-validation`
|
|
91
91
|
|
|
92
|
-
Agents and skills install for the harnesses you pick during global installation: a multi-select offers GitHub Copilot and Claude Code pre-checked, plus OpenAI Codex, Cursor, Gemini CLI, opencode,
|
|
92
|
+
Agents and skills install for the harnesses you pick during global installation: a multi-select offers GitHub Copilot and Claude Code pre-checked, plus OpenAI Codex, Cursor, Gemini CLI, opencode, Pi Coding Agent, Oh My Pi (OMP), Cline, Roo Code, and Windsurf / Devin Desktop. In non-interactive installs they are installed for GitHub Copilot only; run `sap-ai-dev --setup` afterwards to configure destinations or local SAP GUI systems. Repository-scoped installation instructions appear below.
|
|
93
93
|
|
|
94
94
|
## ✨ Your SAP development cockpit, inside your AI assistant
|
|
95
95
|
|
|
@@ -334,7 +334,7 @@ sap-ai-dev --setup
|
|
|
334
334
|
5. Choose Windows / smart card SSO or username/password when the prompt is available.
|
|
335
335
|
6. Start each generated MCP server from VS Code's **MCP: List Servers** command.
|
|
336
336
|
|
|
337
|
-
The same postinstall offers the bundled agents and skills for several AI coding harnesses in a checkbox picker. **GitHub Copilot** and **Claude Code** start pre-checked, so pressing **Enter** alone installs both; **Space** selects or deselects a harness, **a** toggles all
|
|
337
|
+
The same postinstall offers the bundled agents and skills for several AI coding harnesses in a checkbox picker. **GitHub Copilot** and **Claude Code** start pre-checked, so pressing **Enter** alone installs both; **Space** selects or deselects a harness, **a** toggles all eleven, and confirming with none selected skips the step without changing files. When MCP setup succeeds, generated managed MCP servers are also mirrored into selected harnesses with documented user-level JSON configuration (Claude Code, Cursor, Gemini CLI, Pi Coding Agent, OMP, and Windsurf / Devin Desktop). GitHub Copilot continues to use the VS Code/BAS MCP configuration written during setup. With no interactive terminal, the assets are installed for GitHub Copilot only; set `SAP_AI_DEV_TOOLKIT_HARNESSES` to a comma-separated list of harness ids (`github-copilot`, `claude-code`, `codex`, `cursor`, `gemini-cli`, `opencode`, `pi-coding-agent`, `omp`, `cline`, `roo-code`, `windsurf`) to choose non-interactively.
|
|
338
338
|
|
|
339
339
|
Some current npm versions also require install hooks to be approved. If npm reports that `sap-ai-dev-toolkit`'s `postinstall` was blocked, allow it during a fresh install with `npm install --global --allow-scripts=sap-ai-dev-toolkit sap-ai-dev-toolkit`, or run `sap-ai-dev --setup` from an interactive terminal after installation.
|
|
340
340
|
|
|
@@ -418,6 +418,10 @@ During npm postinstall, or after interactive `sap-ai-dev --setup` in BAS or on a
|
|
|
418
418
|
| Gemini CLI | `$HOME/.gemini/skills/` | — | `$HOME/.gemini/settings.json` |
|
|
419
419
|
| opencode | `$XDG_CONFIG_HOME/opencode/skills/` (default `$HOME/.config/opencode/skills/`) | `$XDG_CONFIG_HOME/opencode/agents/<id>.md` (as subagents) | Not changed automatically |
|
|
420
420
|
| Pi Coding Agent | `$PI_CODING_AGENT_DIR/skills/` (default `$HOME/.pi/agent/skills/`) | `$PI_CODING_AGENT_DIR/prompts/<id>.md` (as `/` prompt commands) | `$PI_CODING_AGENT_DIR/mcp.json` (default `$HOME/.pi/agent/mcp.json`) |
|
|
421
|
+
| Oh My Pi (OMP) | `$HOME/.omp/agent/skills/` (overridden by `PI_CONFIG_DIR` or `PI_CODING_AGENT_DIR`) | — | `$HOME/.omp/agent/mcp.json` (same overrides) |
|
|
422
|
+
| Cline | `$HOME/.cline/skills/` | — | Not changed automatically |
|
|
423
|
+
| Roo Code | `$HOME/.roo/skills/` | — | Not changed automatically |
|
|
424
|
+
| Windsurf / Devin Desktop | `$XDG_CONFIG_HOME/devin/skills/` (default `$HOME/.config/devin/skills/`); Windows: `$HOME/.codeium/windsurf/skills/` | — | `$XDG_CONFIG_HOME/devin/mcp_config.json` (default `$HOME/.config/devin/mcp_config.json`); Windows: `%APPDATA%\devin\mcp_config.json` |
|
|
421
425
|
|
|
422
426
|
All 14 skills use the portable `SKILL.md` format and are installed verbatim. The 5 agents use Copilot's `.agent.md` format, so they are installed as native custom agents for harnesses with a custom-agent mechanism (GitHub Copilot, Claude Code, opencode), with frontmatter adapted where needed. Pi receives equivalent prompt templates (`/abap-developer`, `/sap-solution-architect`, and so on) because Pi exposes reusable personas through prompts rather than a separate custom-agent file type. Selecting several harnesses duplicates the small markdown payloads on disk.
|
|
423
427
|
|
package/dist/checksums.txt
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
f10b759b5e320c6fec4c176ef9bce49f783d796eeb8a2476dd5898c94cb14afd vsp-linux-x64
|
|
2
|
+
a4c47e4c15538cc16b540d19f55771e4c1340389efeca815e1881eafd89e45c5 vsp-linux-arm64
|
|
3
|
+
20a197921c88f539ea1a7f5236509dbaa9b9f906a6333e6fb1dce9f8b5eec85c vsp-darwin-x64
|
|
4
|
+
24a8810a17d31a434b92957ea41e6347b24e2a6a35e47b3b2fc13a16082a44c3 vsp-darwin-arm64
|
|
5
|
+
b318fdf5194a7f15ebcf7ee912fb2493df21aba337540a2402f81786300fd0ea vsp-win32-x64.exe
|
package/dist/vsp-darwin-arm64
CHANGED
|
Binary file
|
package/dist/vsp-darwin-x64
CHANGED
|
Binary file
|
package/dist/vsp-linux-arm64
CHANGED
|
Binary file
|
package/dist/vsp-linux-x64
CHANGED
|
Binary file
|
package/dist/vsp-win32-x64.exe
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sap-ai-dev-toolkit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.9909",
|
|
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",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@abaplint/core": "2.119.0",
|
|
12
|
-
"@modelcontextprotocol/sdk": "1.
|
|
12
|
+
"@modelcontextprotocol/sdk": "1.32.1",
|
|
13
13
|
"@sap/hana-client": "2.30.27",
|
|
14
14
|
"terminal-kit": "^3.1.4",
|
|
15
15
|
"undici": "^6.21.3",
|
|
@@ -620,10 +620,30 @@ index 0000000..5e880bd
|
|
|
620
620
|
+ }
|
|
621
621
|
+}
|
|
622
622
|
diff --git a/pkg/adt/http.go b/pkg/adt/http.go
|
|
623
|
-
index 0b4d9f1..
|
|
623
|
+
index 0b4d9f1..f0b5b51 100755
|
|
624
624
|
--- a/pkg/adt/http.go
|
|
625
625
|
+++ b/pkg/adt/http.go
|
|
626
|
-
@@ -
|
|
626
|
+
@@ -313,7 +313,7 @@ func (t *Transport) request(ctx context.Context, path string, opts *RequestOptio
|
|
627
|
+
// The same expiry reaches a plain read as a successful-looking response that
|
|
628
|
+
// was in fact served by the identity provider. Nothing downstream would
|
|
629
|
+
// recognise the logon page it carries, so catch it here by origin.
|
|
630
|
+
- if resp.StatusCode < 400 && t.canReauth() && t.redirectedAwayFromSAP(resp) {
|
|
631
|
+
+ if t.canReauth() && ((resp.StatusCode < 400 && t.redirectedAwayFromSAP(resp)) || isSSOLoginResponse(resp, body)) {
|
|
632
|
+
t.setCSRFToken("")
|
|
633
|
+
t.setSessionID("")
|
|
634
|
+
if err := t.callReauthFunc(ctx); err != nil {
|
|
635
|
+
@@ -448,6 +448,10 @@ func (t *Transport) retryRequest(ctx context.Context, path string, opts *Request
|
|
636
|
+
}
|
|
637
|
+
traceHTTPResponse(resp, body)
|
|
638
|
+
|
|
639
|
+
+ if t.canReauth() && (t.redirectedAwayFromSAP(resp) || isSSOLoginResponse(resp, body)) {
|
|
640
|
+
+ return nil, fmt.Errorf("SSO session is still unauthenticated after re-authentication on %s", path)
|
|
641
|
+
+ }
|
|
642
|
+
+
|
|
643
|
+
if resp.StatusCode >= 400 {
|
|
644
|
+
return nil, &APIError{
|
|
645
|
+
StatusCode: resp.StatusCode,
|
|
646
|
+
@@ -499,6 +503,7 @@ func (t *Transport) fetchCSRFTokenFor(ctx context.Context, stateful bool) error
|
|
627
647
|
// allowReauth exists to break a cycle: re-authenticating ends with a token
|
|
628
648
|
// fetch of its own, and that fetch must not start another re-authentication.
|
|
629
649
|
func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bool, stateful bool) error {
|
|
@@ -631,7 +651,7 @@ index 0b4d9f1..3473536 100755
|
|
|
631
651
|
token, status, redirected, err := t.probeCSRFToken(ctx, http.MethodHead, stateful)
|
|
632
652
|
if err != nil {
|
|
633
653
|
return err
|
|
634
|
-
@@ -513,6 +
|
|
654
|
+
@@ -513,6 +518,29 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
|
|
635
655
|
if err != nil {
|
|
636
656
|
return err
|
|
637
657
|
}
|
|
@@ -661,7 +681,16 @@ index 0b4d9f1..3473536 100755
|
|
|
661
681
|
if !isCSRFToken(token) {
|
|
662
682
|
// An expired SSO session rarely announces itself as a 401. ICF sends
|
|
663
683
|
// the request on to the identity provider, the redirect chain is
|
|
664
|
-
@@ -
|
|
684
|
+
@@ -520,7 +548,7 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
|
|
685
|
+
// 200 — with no CSRF token in it, because it is not ADT answering.
|
|
686
|
+
// A live ADT session always yields a token, so its absence here is
|
|
687
|
+
// the signal, and a hop to a foreign host is the confirmation.
|
|
688
|
+
- if allowReauth && t.canReauth() && getStatus != http.StatusForbidden {
|
|
689
|
+
+ if allowReauth && t.canReauth() && (getStatus != http.StatusForbidden || redirected || getRedirected) {
|
|
690
|
+
reason := fmt.Sprintf("no CSRF token (HEAD %d, GET %d)", status, getStatus)
|
|
691
|
+
if redirected || getRedirected {
|
|
692
|
+
reason = "the identity provider answered instead of SAP"
|
|
693
|
+
@@ -537,6 +565,9 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
|
|
665
694
|
}
|
|
666
695
|
return t.fetchCSRFTokenWithReauth(ctx, false, stateful)
|
|
667
696
|
}
|
|
@@ -671,7 +700,7 @@ index 0b4d9f1..3473536 100755
|
|
|
671
700
|
switch getStatus {
|
|
672
701
|
case http.StatusUnauthorized:
|
|
673
702
|
return fmt.Errorf("authentication failed (401): check username/password")
|
|
674
|
-
@@ -556,7 +
|
|
703
|
+
@@ -556,7 +587,11 @@ func (t *Transport) fetchCSRFTokenWithReauth(ctx context.Context, allowReauth bo
|
|
675
704
|
// returns the token (empty when the server did not supply one), the status, and
|
|
676
705
|
// whether the answer came from somewhere other than the SAP host.
|
|
677
706
|
func (t *Transport) probeCSRFToken(ctx context.Context, method string, stateful bool) (token string, status int, redirected bool, err error) {
|
|
@@ -684,6 +713,42 @@ index 0b4d9f1..3473536 100755
|
|
|
684
713
|
if err != nil {
|
|
685
714
|
return "", 0, false, fmt.Errorf("building URL: %w", err)
|
|
686
715
|
}
|
|
716
|
+
@@ -596,12 +631,34 @@ func (t *Transport) probeCSRFToken(ctx context.Context, method string, stateful
|
|
717
|
+
return "", 0, false, fmt.Errorf("executing request: %w", err)
|
|
718
|
+
}
|
|
719
|
+
defer resp.Body.Close()
|
|
720
|
+
+ body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
|
721
|
+
+ if err != nil {
|
|
722
|
+
+ return "", 0, false, fmt.Errorf("reading CSRF response: %w", err)
|
|
723
|
+
+ }
|
|
724
|
+
// Drain the body so the connection can be reused.
|
|
725
|
+
_, _ = io.Copy(io.Discard, resp.Body)
|
|
726
|
+
traceHTTPResponse(resp, nil)
|
|
727
|
+
t.adoptServerCookies(resp)
|
|
728
|
+
|
|
729
|
+
- return resp.Header.Get("X-CSRF-Token"), resp.StatusCode, t.redirectedAwayFromSAP(resp), nil
|
|
730
|
+
+ return resp.Header.Get("X-CSRF-Token"), resp.StatusCode, t.redirectedAwayFromSAP(resp) || isSSOLoginResponse(resp, body), nil
|
|
731
|
+
+}
|
|
732
|
+
+
|
|
733
|
+
+func isSSOLoginResponse(resp *http.Response, body []byte) bool {
|
|
734
|
+
+ if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusUnauthorized && resp.StatusCode != http.StatusForbidden {
|
|
735
|
+
+ return false
|
|
736
|
+
+ }
|
|
737
|
+
+ page := strings.ToLower(strings.TrimSpace(string(body)))
|
|
738
|
+
+ html := strings.Contains(strings.ToLower(resp.Header.Get("Content-Type")), "text/html") ||
|
|
739
|
+
+ strings.HasPrefix(page, "<!doctype html") || strings.HasPrefix(page, "<html")
|
|
740
|
+
+ if !html || !strings.Contains(page, "<form") {
|
|
741
|
+
+ return false
|
|
742
|
+
+ }
|
|
743
|
+
+ for _, marker := range []string{"samlrequest", "samlresponse", "sap-system-login", "sap-password", `type="password"`, `type='password'`} {
|
|
744
|
+
+ if strings.Contains(page, marker) {
|
|
745
|
+
+ return true
|
|
746
|
+
+ }
|
|
747
|
+
+ }
|
|
748
|
+
+ return false
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// redirectedAwayFromSAP reports whether a response was ultimately served by
|
|
687
752
|
diff --git a/pkg/adt/http_csrf_discovery_test.go b/pkg/adt/http_csrf_discovery_test.go
|
|
688
753
|
new file mode 100644
|
|
689
754
|
index 0000000..80741da
|
|
@@ -746,6 +811,98 @@ index 0000000..80741da
|
|
|
746
811
|
+ }
|
|
747
812
|
+}
|
|
748
813
|
+
|
|
814
|
+
diff --git a/pkg/adt/http_sso_same_host_test.go b/pkg/adt/http_sso_same_host_test.go
|
|
815
|
+
new file mode 100644
|
|
816
|
+
index 0000000..0c39b96
|
|
817
|
+
--- /dev/null
|
|
818
|
+
+++ b/pkg/adt/http_sso_same_host_test.go
|
|
819
|
+
@@ -0,0 +1,86 @@
|
|
820
|
+
+package adt
|
|
821
|
+
+
|
|
822
|
+
+import (
|
|
823
|
+
+ "context"
|
|
824
|
+
+ "io"
|
|
825
|
+
+ "net/http"
|
|
826
|
+
+ "net/http/httptest"
|
|
827
|
+
+ "strings"
|
|
828
|
+
+ "testing"
|
|
829
|
+
+)
|
|
830
|
+
+
|
|
831
|
+
+func TestSameHostLoginPageRefreshesSSO(t *testing.T) {
|
|
832
|
+
+ for _, method := range []string{http.MethodGet, http.MethodPost} {
|
|
833
|
+
+ for _, status := range []int{http.StatusOK, http.StatusUnauthorized, http.StatusForbidden} {
|
|
834
|
+
+ t.Run(method+"/"+http.StatusText(status), func(t *testing.T) {
|
|
835
|
+
+ refreshes := 0
|
|
836
|
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
837
|
+
+ cookie, err := r.Cookie("SAP_SESSIONID")
|
|
838
|
+
+ if err != nil || cookie.Value != "fresh" {
|
|
839
|
+
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
840
|
+
+ w.WriteHeader(status)
|
|
841
|
+
+ _, _ = io.WriteString(w, `<html><body><form action="/sap/saml2/sp/acs"><input name="SAMLRequest" value="fixture"></form></body></html>`)
|
|
842
|
+
+ return
|
|
843
|
+
+ }
|
|
844
|
+
+ w.Header().Set("X-CSRF-Token", "fresh-token")
|
|
845
|
+
+ w.Header().Set("Content-Type", "application/xml")
|
|
846
|
+
+ _, _ = io.WriteString(w, "<source>authenticated</source>")
|
|
847
|
+
+ }))
|
|
848
|
+
+ defer server.Close()
|
|
849
|
+
+ transport := NewTransportWithClient(&Config{
|
|
850
|
+
+ BaseURL: server.URL,
|
|
851
|
+
+ Cookies: map[string]string{"SAP_SESSIONID": "expired"},
|
|
852
|
+
+ ReauthFunc: func(context.Context) (map[string]string, error) {
|
|
853
|
+
+ refreshes++
|
|
854
|
+
+ return map[string]string{"SAP_SESSIONID": "fresh"}, nil
|
|
855
|
+
+ },
|
|
856
|
+
+ }, server.Client())
|
|
857
|
+
+ response, err := transport.Request(context.Background(), "/sap/bc/adt/source", &RequestOptions{Method: method})
|
|
858
|
+
+ if err != nil {
|
|
859
|
+
+ t.Fatalf("request failed: %v", err)
|
|
860
|
+
+ }
|
|
861
|
+
+ if refreshes != 1 || string(response.Body) != "<source>authenticated</source>" {
|
|
862
|
+
+ t.Fatalf("refreshes=%d body=%q, want one SSO refresh and authenticated source", refreshes, response.Body)
|
|
863
|
+
+ }
|
|
864
|
+
+ })
|
|
865
|
+
+ }
|
|
866
|
+
+ }
|
|
867
|
+
+}
|
|
868
|
+
+
|
|
869
|
+
+func TestOrdinaryForbiddenDoesNotRefreshSSO(t *testing.T) {
|
|
870
|
+
+ refreshes := 0
|
|
871
|
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
872
|
+
+ w.WriteHeader(http.StatusForbidden)
|
|
873
|
+
+ _, _ = io.WriteString(w, "Missing development authorization")
|
|
874
|
+
+ }))
|
|
875
|
+
+ defer server.Close()
|
|
876
|
+
+ transport := NewTransportWithClient(&Config{BaseURL: server.URL, ReauthFunc: func(context.Context) (map[string]string, error) {
|
|
877
|
+
+ refreshes++
|
|
878
|
+
+ return nil, nil
|
|
879
|
+
+ }}, server.Client())
|
|
880
|
+
+ _, err := transport.Request(context.Background(), "/sap/bc/adt/source", &RequestOptions{Method: http.MethodGet})
|
|
881
|
+
+ if err == nil || refreshes != 0 {
|
|
882
|
+
+ t.Fatalf("err=%v refreshes=%d, want authorization error without SSO refresh", err, refreshes)
|
|
883
|
+
+ }
|
|
884
|
+
+}
|
|
885
|
+
+
|
|
886
|
+
+func TestSSOLoginPageAfterRefreshFailsWithoutLoop(t *testing.T) {
|
|
887
|
+
+ refreshes := 0
|
|
888
|
+
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
889
|
+
+ if r.URL.Path == "/sap/bc/adt/source" {
|
|
890
|
+
+ w.Header().Set("Content-Type", "text/html")
|
|
891
|
+
+ _, _ = io.WriteString(w, `<html><form><input type="password"></form></html>`)
|
|
892
|
+
+ return
|
|
893
|
+
+ }
|
|
894
|
+
+ w.Header().Set("X-CSRF-Token", "fresh-token")
|
|
895
|
+
+ }))
|
|
896
|
+
+ defer server.Close()
|
|
897
|
+
+ transport := NewTransportWithClient(&Config{BaseURL: server.URL, ReauthFunc: func(context.Context) (map[string]string, error) {
|
|
898
|
+
+ refreshes++
|
|
899
|
+
+ return map[string]string{"SAP_SESSIONID": "fresh"}, nil
|
|
900
|
+
+ }}, server.Client())
|
|
901
|
+
+ _, err := transport.Request(context.Background(), "/sap/bc/adt/source", &RequestOptions{Method: http.MethodGet})
|
|
902
|
+
+ if err == nil || !strings.Contains(err.Error(), "still unauthenticated") || refreshes != 1 {
|
|
903
|
+
+ t.Fatalf("err=%v refreshes=%d, want explicit failure after one refresh", err, refreshes)
|
|
904
|
+
+ }
|
|
905
|
+
+}
|
|
749
906
|
diff --git a/pkg/adt/servicebinding_publish_test.go b/pkg/adt/servicebinding_publish_test.go
|
|
750
907
|
new file mode 100644
|
|
751
908
|
index 0000000..93c19f9
|
package/src/abaplint.mjs
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, realpath } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
1
3
|
import { Config, MemoryFile, Registry } from '@abaplint/core';
|
|
2
4
|
|
|
5
|
+
const MAX_PROJECT_FILES = 20_000;
|
|
6
|
+
const MAX_PROJECT_FILE_BYTES = 2 * 1024 * 1024;
|
|
7
|
+
const MAX_PROJECT_SOURCE_BYTES = 32 * 1024 * 1024;
|
|
8
|
+
const IGNORED_DIRECTORIES = new Set(['.git', '.hg', '.svn', '.build', '.next', '.cache', 'coverage', 'dist', 'node_modules']);
|
|
9
|
+
const PROJECT_CONFIG_FILES = ['.abaplint.jsonc', 'abaplint.jsonc', '.abaplint.json', 'abaplint.json'];
|
|
10
|
+
|
|
3
11
|
export const ABAP_LINT_TOOL = {
|
|
4
12
|
name: 'LintABAP',
|
|
5
13
|
description: 'Run abaplint on caller-supplied source files in memory; does not read workspace files or call SAP.',
|
|
@@ -26,6 +34,21 @@ export const ABAP_LINT_TOOL = {
|
|
|
26
34
|
}
|
|
27
35
|
};
|
|
28
36
|
|
|
37
|
+
export const ABAP_LINT_PROJECT_TOOL = {
|
|
38
|
+
name: 'LintABAPProject',
|
|
39
|
+
description: 'Run read-only abaplint checks over serialized ABAP objects under the configured workspace root. Loads the project config and local dependency folders when present; does not call SAP, write files, or activate ABAP.',
|
|
40
|
+
inputSchema: {
|
|
41
|
+
type: 'object',
|
|
42
|
+
properties: {
|
|
43
|
+
directory: { type: 'string', description: 'Optional project subdirectory relative to the configured workspace root.' },
|
|
44
|
+
config: { type: 'object', description: 'Optional abaplint config object that overrides project config discovery.' }
|
|
45
|
+
},
|
|
46
|
+
required: [],
|
|
47
|
+
additionalProperties: false
|
|
48
|
+
},
|
|
49
|
+
annotations: { readOnlyHint: true }
|
|
50
|
+
};
|
|
51
|
+
|
|
29
52
|
function isObject(value) {
|
|
30
53
|
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
31
54
|
}
|
|
@@ -65,6 +88,170 @@ function errorResult(error) {
|
|
|
65
88
|
};
|
|
66
89
|
}
|
|
67
90
|
|
|
91
|
+
function reportResult(report) {
|
|
92
|
+
return {
|
|
93
|
+
isError: false,
|
|
94
|
+
content: [{ type: 'text', text: JSON.stringify(report) }],
|
|
95
|
+
structuredContent: report
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function lintReport(registry, filesChecked) {
|
|
100
|
+
const issues = registry.findIssues();
|
|
101
|
+
const counts = { Error: 0, Warning: 0, Info: 0 };
|
|
102
|
+
const reportIssues = issues.map(issue => {
|
|
103
|
+
counts[issue.getSeverity()] += 1;
|
|
104
|
+
const start = issue.getStart();
|
|
105
|
+
const end = issue.getEnd();
|
|
106
|
+
return {
|
|
107
|
+
filename: issue.getFilename(),
|
|
108
|
+
rule: issue.getKey(),
|
|
109
|
+
severity: issue.getSeverity(),
|
|
110
|
+
message: issue.getMessage(),
|
|
111
|
+
start: { line: start.getRow(), column: start.getCol() },
|
|
112
|
+
end: { line: end.getRow(), column: end.getCol() }
|
|
113
|
+
};
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
status: reportIssues.length === 0 ? 'clean' : 'issues',
|
|
117
|
+
filesChecked,
|
|
118
|
+
issueCount: reportIssues.length,
|
|
119
|
+
errors: counts.Error,
|
|
120
|
+
warnings: counts.Warning,
|
|
121
|
+
infos: counts.Info,
|
|
122
|
+
issues: reportIssues
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pathIsWithin(parent, candidate) {
|
|
127
|
+
const path = relative(parent, candidate);
|
|
128
|
+
return path === '' || (path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function validateProjectArguments(arguments_) {
|
|
132
|
+
if (!isObject(arguments_)) throw new Error('Arguments must be an object');
|
|
133
|
+
rejectUnknownFields(arguments_, new Set(['directory', 'config']), 'argument');
|
|
134
|
+
if (arguments_.directory !== undefined) {
|
|
135
|
+
if (typeof arguments_.directory !== 'string' || arguments_.directory.includes('\0') ||
|
|
136
|
+
isAbsolute(arguments_.directory) || /^[A-Za-z]:[\\/]/.test(arguments_.directory) ||
|
|
137
|
+
arguments_.directory.split(/[\\/]+/).includes('..')) {
|
|
138
|
+
throw new Error('directory must be a relative path inside the configured workspace root');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (arguments_.config !== undefined && !isObject(arguments_.config)) {
|
|
142
|
+
throw new Error('config must be an object');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async function loadProjectConfig(directory, override) {
|
|
147
|
+
if (override !== undefined) return { config: new Config(JSON.stringify(override)), source: 'caller' };
|
|
148
|
+
for (const filename of PROJECT_CONFIG_FILES) {
|
|
149
|
+
const configPath = resolve(directory, filename);
|
|
150
|
+
let info;
|
|
151
|
+
try { info = await lstat(configPath); }
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (error?.code === 'ENOENT') continue;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
if (info.isSymbolicLink() || !info.isFile()) throw new Error(`${filename} must be a regular file inside the workspace`);
|
|
157
|
+
try {
|
|
158
|
+
return { config: new Config(await readFile(configPath, 'utf8')), source: filename };
|
|
159
|
+
} catch (error) {
|
|
160
|
+
throw new Error(`Could not load ${filename}: ${error.message || error}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return { config: Config.getDefault(), source: 'default' };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function enableSyntaxRules(config) {
|
|
167
|
+
const projectConfig = config.get();
|
|
168
|
+
const rules = { ...(projectConfig.rules || {}) };
|
|
169
|
+
const disabled = [];
|
|
170
|
+
for (const rule of ['parser_error', 'check_syntax']) {
|
|
171
|
+
if (rules[rule] === false) disabled.push(rule);
|
|
172
|
+
else if (rules[rule] === undefined) rules[rule] = true;
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
config: new Config(JSON.stringify({ ...projectConfig, rules })),
|
|
176
|
+
disabled
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function dependencyRoots(config, projectRoot) {
|
|
181
|
+
const declared = config.get()?.dependencies;
|
|
182
|
+
if (!Array.isArray(declared)) return { roots: [], missing: [], count: 0 };
|
|
183
|
+
const roots = [];
|
|
184
|
+
const missing = [];
|
|
185
|
+
for (const dependency of declared) {
|
|
186
|
+
const folder = dependency?.folder;
|
|
187
|
+
if (typeof folder !== 'string' || !folder.trim()) {
|
|
188
|
+
missing.push(String(folder || '(unspecified folder)'));
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const relativeFolder = folder.replace(/^[\\/]+/, '');
|
|
192
|
+
if (/^[A-Za-z]:[\\/]/.test(relativeFolder) || relativeFolder.split(/[\\/]+/).includes('..')) {
|
|
193
|
+
throw new Error('Dependency folders must stay inside the configured project directory');
|
|
194
|
+
}
|
|
195
|
+
const candidate = resolve(projectRoot, relativeFolder || '.');
|
|
196
|
+
if (!pathIsWithin(projectRoot, candidate)) throw new Error('Dependency folders must stay inside the configured project directory');
|
|
197
|
+
let canonical;
|
|
198
|
+
try { canonical = await realpath(candidate); }
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (error?.code === 'ENOENT' || error?.code === 'ENOTDIR') {
|
|
201
|
+
missing.push(folder);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
if (!pathIsWithin(projectRoot, canonical)) throw new Error('Dependency folders must stay inside the configured project directory');
|
|
207
|
+
const info = await lstat(canonical);
|
|
208
|
+
if (!info.isDirectory()) {
|
|
209
|
+
missing.push(folder);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
roots.push(canonical);
|
|
213
|
+
}
|
|
214
|
+
return { roots, missing, count: declared.length };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function collectProjectFiles(projectRoot, dependencies) {
|
|
218
|
+
const sourceFiles = [];
|
|
219
|
+
const dependencyFiles = [];
|
|
220
|
+
const pending = [projectRoot];
|
|
221
|
+
let totalBytes = 0;
|
|
222
|
+
while (pending.length) {
|
|
223
|
+
const directory = pending.pop();
|
|
224
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
225
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
const absolutePath = resolve(directory, entry.name);
|
|
228
|
+
if (entry.isDirectory()) {
|
|
229
|
+
const isDependencyPath = dependencies.some(root => pathIsWithin(absolutePath, root) || pathIsWithin(root, absolutePath));
|
|
230
|
+
if (!IGNORED_DIRECTORIES.has(entry.name) || isDependencyPath) pending.push(absolutePath);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
if (!entry.isFile()) continue;
|
|
234
|
+
const filename = relative(projectRoot, absolutePath).split(sep).join('/');
|
|
235
|
+
const basename = filename.split('/').at(-1);
|
|
236
|
+
const nameParts = basename.split('.');
|
|
237
|
+
if (nameParts.length < 3 || !nameParts[0] || !nameParts[1]) continue;
|
|
238
|
+
if (!new MemoryFile(filename, '').getObjectType()) continue;
|
|
239
|
+
const info = await lstat(absolutePath);
|
|
240
|
+
if (!info.isFile() || info.isSymbolicLink()) continue;
|
|
241
|
+
if (info.size > MAX_PROJECT_FILE_BYTES) throw new Error(`ABAP file exceeds ${MAX_PROJECT_FILE_BYTES} bytes: ${filename}`);
|
|
242
|
+
totalBytes += info.size;
|
|
243
|
+
if (totalBytes > MAX_PROJECT_SOURCE_BYTES) throw new Error(`Workspace ABAP sources exceed ${MAX_PROJECT_SOURCE_BYTES} bytes`);
|
|
244
|
+
const file = new MemoryFile(filename, await readFile(absolutePath, 'utf8'));
|
|
245
|
+
const isDependency = dependencies.some(root => pathIsWithin(root, absolutePath));
|
|
246
|
+
(isDependency ? dependencyFiles : sourceFiles).push(file);
|
|
247
|
+
if (sourceFiles.length + dependencyFiles.length > MAX_PROJECT_FILES) {
|
|
248
|
+
throw new Error(`Workspace contains more than ${MAX_PROJECT_FILES} ABAP files`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return { sourceFiles, dependencyFiles };
|
|
253
|
+
}
|
|
254
|
+
|
|
68
255
|
export async function runABAPLint(arguments_) {
|
|
69
256
|
try {
|
|
70
257
|
validateArguments(arguments_);
|
|
@@ -74,31 +261,49 @@ export async function runABAPLint(arguments_) {
|
|
|
74
261
|
const files = arguments_.files.map(({ filename, source }) => new MemoryFile(filename, source));
|
|
75
262
|
const registry = new Registry(config).addFiles(files);
|
|
76
263
|
await registry.parseAsync();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
264
|
+
return { isError: false, content: [{ type: 'text', text: JSON.stringify(lintReport(registry, files.length)) }] };
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return errorResult(error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export async function runABAPLintProject(arguments_, { workspaceRoot = process.cwd() } = {}) {
|
|
271
|
+
try {
|
|
272
|
+
validateProjectArguments(arguments_);
|
|
273
|
+
const root = await realpath(workspaceRoot);
|
|
274
|
+
const directory = resolve(root, arguments_.directory || '.');
|
|
275
|
+
if (!pathIsWithin(root, directory)) throw new Error('directory must stay inside the configured workspace root');
|
|
276
|
+
const projectRoot = await realpath(directory);
|
|
277
|
+
if (!pathIsWithin(root, projectRoot)) throw new Error('directory must stay inside the configured workspace root');
|
|
278
|
+
const projectInfo = await lstat(projectRoot);
|
|
279
|
+
if (!projectInfo.isDirectory()) throw new Error('directory must identify a workspace directory');
|
|
280
|
+
|
|
281
|
+
const { config: projectConfig, source: configSource } = await loadProjectConfig(projectRoot, arguments_.config);
|
|
282
|
+
const { config, disabled: disabledSyntaxRules } = enableSyntaxRules(projectConfig);
|
|
283
|
+
const dependencies = await dependencyRoots(config, projectRoot);
|
|
284
|
+
const { sourceFiles, dependencyFiles } = await collectProjectFiles(projectRoot, dependencies.roots);
|
|
285
|
+
const registry = new Registry(config);
|
|
286
|
+
registry.addDependencies(dependencyFiles);
|
|
287
|
+
registry.addFiles(sourceFiles);
|
|
288
|
+
await registry.parseAsync();
|
|
289
|
+
|
|
290
|
+
const report = lintReport(registry, sourceFiles.length);
|
|
291
|
+
if (report.filesChecked === 0) report.status = 'no-abap-files';
|
|
292
|
+
else if (report.status === 'clean' && (dependencies.missing.length || disabledSyntaxRules.length)) report.status = 'incomplete';
|
|
293
|
+
return reportResult({
|
|
294
|
+
...report,
|
|
295
|
+
scope: 'workspace-static',
|
|
296
|
+
activation: 'not-performed',
|
|
297
|
+
workspaceDirectory: arguments_.directory || '.',
|
|
298
|
+
configSource,
|
|
299
|
+
targetRelease: config.getVersion(),
|
|
300
|
+
syntaxRules: { enabled: ['parser_error', 'check_syntax'].filter(rule => !disabledSyntaxRules.includes(rule)), disabled: disabledSyntaxRules },
|
|
301
|
+
dependencies: {
|
|
302
|
+
declared: dependencies.count,
|
|
303
|
+
filesLoaded: dependencyFiles.length,
|
|
304
|
+
missingFolders: dependencies.missing
|
|
305
|
+
}
|
|
91
306
|
});
|
|
92
|
-
const report = {
|
|
93
|
-
status: reportIssues.length === 0 ? 'clean' : 'issues',
|
|
94
|
-
filesChecked: files.length,
|
|
95
|
-
issueCount: reportIssues.length,
|
|
96
|
-
errors: counts.Error,
|
|
97
|
-
warnings: counts.Warning,
|
|
98
|
-
infos: counts.Info,
|
|
99
|
-
issues: reportIssues
|
|
100
|
-
};
|
|
101
|
-
return { isError: false, content: [{ type: 'text', text: JSON.stringify(report) }] };
|
|
102
307
|
} catch (error) {
|
|
103
308
|
return errorResult(error);
|
|
104
309
|
}
|
|
@@ -7,7 +7,7 @@ import { decodeJsonFileBuffer } from './mcp-config.mjs';
|
|
|
7
7
|
|
|
8
8
|
const MANAGED_ENV = 'SAP_AI_DEV_TOOLKIT_MANAGED';
|
|
9
9
|
|
|
10
|
-
function mcpServersPath(harness, { home = process.env.HOME || homedir(), env = process.env } = {}) {
|
|
10
|
+
function mcpServersPath(harness, { home = process.env.HOME || homedir(), env = process.env, platform = process.platform } = {}) {
|
|
11
11
|
switch (harness.id) {
|
|
12
12
|
case 'claude-code':
|
|
13
13
|
return join(home, '.claude.json');
|
|
@@ -15,8 +15,16 @@ function mcpServersPath(harness, { home = process.env.HOME || homedir(), env = p
|
|
|
15
15
|
return join(harnessRoot(harness, { home, env }), 'mcp.json');
|
|
16
16
|
case 'gemini-cli':
|
|
17
17
|
return join(harnessRoot(harness, { home, env }), 'settings.json');
|
|
18
|
+
case 'omp':
|
|
19
|
+
return join(harnessRoot(harness, { home, env }), 'mcp.json');
|
|
18
20
|
case 'pi-coding-agent':
|
|
19
21
|
return join(harnessRoot(harness, { home, env }), 'mcp.json');
|
|
22
|
+
case 'windsurf': {
|
|
23
|
+
const configRoot = platform === 'win32'
|
|
24
|
+
? (typeof env?.APPDATA === 'string' && env.APPDATA.trim() ? env.APPDATA.trim() : join(home, 'AppData', 'Roaming'))
|
|
25
|
+
: (typeof env?.XDG_CONFIG_HOME === 'string' && env.XDG_CONFIG_HOME.trim() ? env.XDG_CONFIG_HOME.trim() : join(home, '.config'));
|
|
26
|
+
return join(configRoot, 'devin', 'mcp_config.json');
|
|
27
|
+
}
|
|
20
28
|
default:
|
|
21
29
|
return null;
|
|
22
30
|
}
|
package/src/harnesses.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { homedir } from 'node:os';
|
|
2
|
-
import { join } from 'node:path';
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
3
|
|
|
4
4
|
export const HARNESS_ENV_VAR = 'SAP_AI_DEV_TOOLKIT_HARNESSES';
|
|
5
5
|
export const DEFAULT_HARNESS_IDS = ['github-copilot', 'claude-code'];
|
|
@@ -38,6 +38,19 @@ export function piCodingAgentRoot({ home = process.env.HOME || homedir(), env =
|
|
|
38
38
|
return override || join(home, '.pi', 'agent');
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export function ompAgentRoot({ home = process.env.HOME || homedir(), env = process.env } = {}) {
|
|
42
|
+
const agentOverride = typeof env?.PI_CODING_AGENT_DIR === 'string' ? env.PI_CODING_AGENT_DIR.trim() : '';
|
|
43
|
+
if (agentOverride) return resolve(agentOverride);
|
|
44
|
+
const configDirectory = typeof env?.PI_CONFIG_DIR === 'string' ? env.PI_CONFIG_DIR.trim() : '';
|
|
45
|
+
return join(home, configDirectory || '.omp', 'agent');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function windsurfSkillsRoot({ home = process.env.HOME || homedir(), env = process.env, platform = process.platform } = {}) {
|
|
49
|
+
if (platform === 'win32') return join(home, '.codeium', 'windsurf');
|
|
50
|
+
const override = typeof env?.XDG_CONFIG_HOME === 'string' ? env.XDG_CONFIG_HOME.trim() : '';
|
|
51
|
+
return join(override || join(home, '.config'), 'devin');
|
|
52
|
+
}
|
|
53
|
+
|
|
41
54
|
export function claudeCodeAgentDocument(source) {
|
|
42
55
|
const frontmatter = splitFrontmatter(source);
|
|
43
56
|
if (!frontmatter) throw new Error('unsupported agent frontmatter');
|
|
@@ -86,7 +99,11 @@ export const HARNESSES = [
|
|
|
86
99
|
{ id: 'cursor', label: 'Cursor', directory: '.cursor', supportsAgents: false },
|
|
87
100
|
{ id: 'gemini-cli', label: 'Gemini CLI', directory: '.gemini', supportsAgents: false },
|
|
88
101
|
{ id: 'opencode', label: 'opencode', directory: 'opencode', configHome: true, supportsAgents: true, agentFileName: agentMarkdownName, agentDocument: opencodeAgentDocument },
|
|
89
|
-
{ id: 'pi-coding-agent', label: 'Pi Coding Agent', directory: '.pi/agent', supportsAgents: true, agentTargetPrefix: 'prompts', agentFileName: agentMarkdownName, agentDocument: piCodingAgentPromptDocument, root: piCodingAgentRoot }
|
|
102
|
+
{ id: 'pi-coding-agent', label: 'Pi Coding Agent', directory: '.pi/agent', supportsAgents: true, agentTargetPrefix: 'prompts', agentFileName: agentMarkdownName, agentDocument: piCodingAgentPromptDocument, root: piCodingAgentRoot },
|
|
103
|
+
{ id: 'omp', label: 'Oh My Pi (OMP)', directory: '.omp/agent', supportsAgents: false, root: ompAgentRoot },
|
|
104
|
+
{ id: 'cline', label: 'Cline', directory: '.cline', supportsAgents: false },
|
|
105
|
+
{ id: 'roo-code', label: 'Roo Code', directory: '.roo', supportsAgents: false },
|
|
106
|
+
{ id: 'windsurf', label: 'Windsurf / Devin Desktop', directory: 'devin', supportsAgents: false, root: windsurfSkillsRoot }
|
|
90
107
|
];
|
|
91
108
|
|
|
92
109
|
export function harnessById(id) {
|
|
@@ -97,8 +114,8 @@ export function harnessById(id) {
|
|
|
97
114
|
return harness;
|
|
98
115
|
}
|
|
99
116
|
|
|
100
|
-
export function harnessRoot(harness, { home = process.env.HOME || homedir(), env = process.env } = {}) {
|
|
101
|
-
if (harness.root) return harness.root({ home, env });
|
|
117
|
+
export function harnessRoot(harness, { home = process.env.HOME || homedir(), env = process.env, platform = process.platform } = {}) {
|
|
118
|
+
if (harness.root) return harness.root({ home, env, platform });
|
|
102
119
|
if (harness.configHome) {
|
|
103
120
|
const override = typeof env?.XDG_CONFIG_HOME === 'string' ? env.XDG_CONFIG_HOME.trim() : '';
|
|
104
121
|
return join(override || join(home, '.config'), harness.directory);
|
package/src/launcher.mjs
CHANGED
|
@@ -107,7 +107,7 @@ async function runDoctor(destinations) {
|
|
|
107
107
|
// unprefixed (get_source, run_query); match the slug-prefixed form too
|
|
108
108
|
// in case this ever runs against a multi-destination server.
|
|
109
109
|
const toolPrefix = `${slugifyDestination(destination.name)}_`;
|
|
110
|
-
const localSegments = new Set(['lint_abap', 'get_application_log', 'prepare_abap_change_set', 'apply_abap_change_set', 'check_transport_readiness', 'plan_abap_cloud_migration', 'generate_rap_regression_suite', 'run_rap_regression_suite']);
|
|
110
|
+
const localSegments = new Set(['lint_abap', 'lint_abap_project', 'get_application_log', 'prepare_abap_change_set', 'apply_abap_change_set', 'check_transport_readiness', 'plan_abap_cloud_migration', 'generate_rap_regression_suite', 'run_rap_regression_suite']);
|
|
111
111
|
const isUpstreamTool = tool => (tool.name.startsWith(toolPrefix) ? tool.name.slice(toolPrefix.length) : tool.name);
|
|
112
112
|
const upstreamCount = tools.filter(tool => !localSegments.has(isUpstreamTool(tool))).length;
|
|
113
113
|
checks.push(doctorRow(destination.name, 'MCP tools/list', upstreamCount ? 'passed' : 'failed', `${upstreamCount} upstream tools and ${tools.length} total MCP tools returned; chat-picker binding is host-managed`));
|
package/src/mcp-proxy.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
2
2
|
import { once } from 'node:events';
|
|
3
3
|
import { sanitizeChildEnv, slugifyDestination } from './bas-discovery.mjs';
|
|
4
|
-
import { ABAP_LINT_TOOL, runABAPLint } from './abaplint.mjs';
|
|
4
|
+
import { ABAP_LINT_PROJECT_TOOL, ABAP_LINT_TOOL, runABAPLint, runABAPLintProject } from './abaplint.mjs';
|
|
5
5
|
import { createEngineeringTools } from './engineering-tools.mjs';
|
|
6
6
|
import { brandedEnvValue } from './branding.mjs';
|
|
7
7
|
import { diagnosticText, redactText } from './redact.mjs';
|
|
@@ -698,6 +698,12 @@ export class MCPProxy {
|
|
|
698
698
|
});
|
|
699
699
|
}
|
|
700
700
|
}
|
|
701
|
+
const projectLintName = snakeCaseName(ABAP_LINT_PROJECT_TOOL.name);
|
|
702
|
+
publish(projectLintName, {
|
|
703
|
+
handler: arguments_ => runABAPLintProject(arguments_, {
|
|
704
|
+
workspaceRoot: brandedEnvValue(this.env, 'WORKSPACE_ROOT') || process.cwd()
|
|
705
|
+
})
|
|
706
|
+
}, { ...ABAP_LINT_PROJECT_TOOL, name: projectLintName });
|
|
701
707
|
if (!merged.length && this.children.length) throw new Error('No destination child provided tools');
|
|
702
708
|
// Swap atomically so a concurrent tools/call never sees a partial map.
|
|
703
709
|
this.namespace = namespace;
|
package/src/setup.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { adtBaseUrl, adtPort, defaultAdtUrl, discoverSapGuiSystems, hasExplicitP
|
|
|
8
8
|
import { discoverCloudFoundryDestinations, getCloudFoundryTarget, deleteManagedCloudFoundryServiceKeys, findOrphanedCloudFoundryServiceKeys } from './cf-destination.mjs';
|
|
9
9
|
import { SAP_DEVELOPMENT_MCP_SERVERS, collectCloudFoundryKeyReferencesFromAllEntries, collectManagedCloudFoundryKeyReferences, installMcpConfig, readMcpConfig, resolveMcpConfigPath } from './mcp-config.mjs';
|
|
10
10
|
import { discoverCertificateDnsNames } from './tls-adt-proxy.mjs';
|
|
11
|
+
import { findRemoteControllableSsoBrowsers } from './sso-browsers.mjs';
|
|
11
12
|
|
|
12
13
|
const SETUP_COMMAND = 'sap-ai-dev --setup';
|
|
13
14
|
|
|
@@ -66,6 +67,45 @@ async function chooseLocalAuthMode(destination, { input = stdin, output = stdout
|
|
|
66
67
|
}, { input, output });
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
function createSsoBrowserSelector({ discoverSsoBrowsers, input, output, env }) {
|
|
71
|
+
let selectedPath;
|
|
72
|
+
let selectionComplete = false;
|
|
73
|
+
return async destination => {
|
|
74
|
+
if (selectionComplete) return selectedPath;
|
|
75
|
+
selectionComplete = true;
|
|
76
|
+
print(output, formatStatus('Checking installed browsers for remote DevTools access.', 'progress', output, 'SSO'));
|
|
77
|
+
let result;
|
|
78
|
+
try {
|
|
79
|
+
result = await discoverSsoBrowsers({ env });
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const reason = String(error?.message || error).replace(/\s+/g, ' ').slice(0, 240);
|
|
82
|
+
print(output, formatStatus(`Browser control check failed (${reason}). Setup will keep automatic browser discovery.`, 'warning', output, 'SSO'));
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const { browsers = [], unavailable = [] } = result || {};
|
|
86
|
+
if (!browsers.length) {
|
|
87
|
+
const details = unavailable.map(browser => `${browser.name}: ${browser.reason}`).join('; ').slice(0, 300);
|
|
88
|
+
print(output, formatStatus(`No remotely controllable SSO browser was found${details ? ` (${details})` : ''}. Setup will keep automatic browser discovery.`, 'warning', output, 'SSO'));
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
if (browsers.length === 1) {
|
|
92
|
+
selectedPath = browsers[0].path;
|
|
93
|
+
print(output, formatStatus(`Using ${browsers[0].name}; it responded to a remote DevTools command.`, 'success', output, 'SSO browser'));
|
|
94
|
+
return selectedPath;
|
|
95
|
+
}
|
|
96
|
+
selectedPath = await selectPrompt({
|
|
97
|
+
message: `Browser for SSO (${destination.name})`,
|
|
98
|
+
defaultValue: browsers[0].path,
|
|
99
|
+
choices: browsers.map((browser, index) => ({
|
|
100
|
+
name: `${browser.name} (${browser.path})`,
|
|
101
|
+
value: browser.path,
|
|
102
|
+
shortcut: String(index + 1)
|
|
103
|
+
}))
|
|
104
|
+
}, { input, output });
|
|
105
|
+
return selectedPath;
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
69
109
|
async function confirmCloudFoundryImport({ input = stdin, output = stdout } = {}) {
|
|
70
110
|
const readline = createInterface({ input, output });
|
|
71
111
|
try {
|
|
@@ -238,7 +278,7 @@ async function confirmPrompt(message, { input = stdin, output = stdout } = {}) {
|
|
|
238
278
|
return !/^n(?:o)?$/i.test(String(answer || '').trim());
|
|
239
279
|
}
|
|
240
280
|
|
|
241
|
-
function buildLocalClientDestination(destination, client, authMode, { multipleClients = false, duplicateSystemName = false } = {}) {
|
|
281
|
+
function buildLocalClientDestination(destination, client, authMode, { multipleClients = false, duplicateSystemName = false, browserPath } = {}) {
|
|
242
282
|
const name = localClientDestinationName(destination, client, { multipleClients, duplicateSystemName });
|
|
243
283
|
const localDestination = { ...destination, name, serverName: name, client };
|
|
244
284
|
const userInputId = `sap-ai-dev-${localDestination.serverName || localDestination.name}-user`.toLowerCase().replace(/[^a-z0-9_-]+/g, '-');
|
|
@@ -248,7 +288,7 @@ function buildLocalClientDestination(destination, client, authMode, { multipleCl
|
|
|
248
288
|
{ id: passwordInputId, type: 'promptString', description: `SAP password for ${localDestination.name}`, password: true }
|
|
249
289
|
];
|
|
250
290
|
if (authMode === 'sso') {
|
|
251
|
-
localDestination.childEnv = { SAP_AUTH_MODE: 'sso' };
|
|
291
|
+
localDestination.childEnv = { SAP_AUTH_MODE: 'sso', ...(browserPath ? { SAP_BROWSER_EXEC: browserPath } : {}) };
|
|
252
292
|
localDestination.inputs = [];
|
|
253
293
|
} else {
|
|
254
294
|
localDestination.childEnv = {
|
|
@@ -285,7 +325,7 @@ async function maybeConfigureTlsSelfHeal(destination, promptLabel, { input = std
|
|
|
285
325
|
// collects the minimal ADT configuration directly from the user and produces
|
|
286
326
|
// the same local destination shape as the SAP GUI flow, so installation,
|
|
287
327
|
// credential inputs, and TLS handling stay identical.
|
|
288
|
-
async function runManualEntryWizard({ env = process.env, input = stdin, output = stdout, detectAdtUrl } = {}) {
|
|
328
|
+
async function runManualEntryWizard({ env = process.env, input = stdin, output = stdout, detectAdtUrl, selectSsoBrowser } = {}) {
|
|
289
329
|
print(output, '');
|
|
290
330
|
print(output, colorText('✍️ Manual system entry — skip discovery, add your SAP system directly', 'magenta', output));
|
|
291
331
|
print(output, colorText(' The ADT URL is the base URL that Eclipse ADT or VS Code ADT tools use, for example https://myhost.example:44300.', 'cyan', output));
|
|
@@ -338,10 +378,11 @@ async function runManualEntryWizard({ env = process.env, input = stdin, output =
|
|
|
338
378
|
} else {
|
|
339
379
|
if (authMode === 'sso') warnSsoIpHost(destination, name, output);
|
|
340
380
|
else await maybeConfigureTlsSelfHeal(destination, name, { input, output });
|
|
381
|
+
const browserPath = authMode === 'sso' ? await selectSsoBrowser(destination) : undefined;
|
|
341
382
|
destination.authentication = localAuthenticationLabel(authMode);
|
|
342
383
|
destination.authMode = authMode;
|
|
343
384
|
for (const client of clients) {
|
|
344
|
-
expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1 }));
|
|
385
|
+
expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1, browserPath }));
|
|
345
386
|
}
|
|
346
387
|
print(output, formatStatus(`Added ${name} (client${clients.length === 1 ? '' : 's'} ${clients.join(', ')}).`, 'success', output, 'Manual entry'));
|
|
347
388
|
}
|
|
@@ -469,7 +510,7 @@ function printConnectionInstructions(output, result) {
|
|
|
469
510
|
|
|
470
511
|
// Checkbox selection over the discovered destinations plus, outside BAS, the
|
|
471
512
|
// per-system confirmation prompts (clients, ADT URL, TLS self-heal, login).
|
|
472
|
-
async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin, output = stdout, env = process.env, detectAdtUrl, previousAdtUrls = new Map() } = {}) {
|
|
513
|
+
async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin, output = stdout, env = process.env, detectAdtUrl, previousAdtUrls = new Map(), selectSsoBrowser } = {}) {
|
|
473
514
|
const orderedDestinations = [...destinations].sort((left, right) => {
|
|
474
515
|
const leftRank = left.disabledReason ? 2 : (left.probe?.available === false ? 1 : 0);
|
|
475
516
|
const rightRank = right.disabledReason ? 2 : (right.probe?.available === false ? 1 : 0);
|
|
@@ -495,7 +536,7 @@ async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin
|
|
|
495
536
|
print(output, colorText(' Space = select/deselect · a = toggle all · m = add a system manually · Enter = confirm.', 'cyan', output));
|
|
496
537
|
print(output, colorText(' 💡 No system listed, or need an extra one? Press m to skip discovery and add any SAP system manually.', 'magenta', output));
|
|
497
538
|
print(output, '');
|
|
498
|
-
const addManually = async () => (await runManualEntryWizard({ env, input, output, detectAdtUrl })).map(destination => ({
|
|
539
|
+
const addManually = async () => (await runManualEntryWizard({ env, input, output, detectAdtUrl, selectSsoBrowser })).map(destination => ({
|
|
499
540
|
value: destination,
|
|
500
541
|
name: `${destination.name} (manual, client ${destination.client})`
|
|
501
542
|
}));
|
|
@@ -549,10 +590,11 @@ async function chooseDiscoveredDestinations(destinations, { isBas, input = stdin
|
|
|
549
590
|
}
|
|
550
591
|
if (authMode === 'sso') warnSsoIpHost(destination, promptLabel, output);
|
|
551
592
|
else await maybeConfigureTlsSelfHeal(destination, promptLabel, { input, output });
|
|
593
|
+
const browserPath = authMode === 'sso' ? await selectSsoBrowser(destination) : undefined;
|
|
552
594
|
destination.authentication = localAuthenticationLabel(authMode);
|
|
553
595
|
destination.authMode = authMode;
|
|
554
596
|
for (const client of clients) {
|
|
555
|
-
expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1, duplicateSystemName }));
|
|
597
|
+
expanded.push(buildLocalClientDestination(destination, client, authMode, { multipleClients: clients.length > 1, duplicateSystemName, browserPath }));
|
|
556
598
|
}
|
|
557
599
|
}
|
|
558
600
|
selected = expanded;
|
|
@@ -570,7 +612,8 @@ export async function runSetup({
|
|
|
570
612
|
confirmCfImport = confirmCloudFoundryImport,
|
|
571
613
|
install = installMcpConfig,
|
|
572
614
|
includeSapDevelopmentToolsPrompt = false,
|
|
573
|
-
detectAdtUrl
|
|
615
|
+
detectAdtUrl,
|
|
616
|
+
discoverSsoBrowsers = findRemoteControllableSsoBrowsers
|
|
574
617
|
} = {}) {
|
|
575
618
|
const isBas = Boolean(env.H2O_URL);
|
|
576
619
|
if (!isBas && (!input.isTTY || !output.isTTY)) {
|
|
@@ -581,6 +624,7 @@ export async function runSetup({
|
|
|
581
624
|
print(output, formatStatus(`Interactive setup needs a terminal. Rerun ${SETUP_COMMAND} from a terminal.`, 'warning', output, 'SAP AI Dev Toolkit'));
|
|
582
625
|
return { skipped: true, reason: 'non-tty' };
|
|
583
626
|
}
|
|
627
|
+
const selectSsoBrowser = createSsoBrowserSelector({ discoverSsoBrowsers, input, output, env });
|
|
584
628
|
print(output, '');
|
|
585
629
|
print(output, colorText('💡 Tip: you can skip auto-discovery and add a system manually — press m in the destination picker, or take the wizard offer when nothing is found.', 'magenta', output));
|
|
586
630
|
if (!isBas) print(output, formatStatus('Looking for local SAP GUI system configuration.', 'progress', output, 'Setup'));
|
|
@@ -721,11 +765,11 @@ export async function runSetup({
|
|
|
721
765
|
]
|
|
722
766
|
}, { input, output });
|
|
723
767
|
if (choice !== 'manual') return await exitWithoutDestinations();
|
|
724
|
-
selected = await runManualEntryWizard({ env, input, output, detectAdtUrl });
|
|
768
|
+
selected = await runManualEntryWizard({ env, input, output, detectAdtUrl, selectSsoBrowser });
|
|
725
769
|
additiveSetup = true;
|
|
726
770
|
if (!selected.length) return await exitWithoutDestinations();
|
|
727
771
|
} else {
|
|
728
|
-
selected = await chooseDiscoveredDestinations(destinations, { isBas, input, output, env, detectAdtUrl, previousAdtUrls });
|
|
772
|
+
selected = await chooseDiscoveredDestinations(destinations, { isBas, input, output, env, detectAdtUrl, previousAdtUrls, selectSsoBrowser });
|
|
729
773
|
additiveSetup = selected.length > 0;
|
|
730
774
|
}
|
|
731
775
|
|
package/src/sso-browsers.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { access, constants } from 'node:fs/promises';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { posix, win32 } from 'node:path';
|
|
1
|
+
import { execFile, spawn } from 'node:child_process';
|
|
2
|
+
import { access, constants, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { homedir, tmpdir } from 'node:os';
|
|
4
|
+
import { join, posix, win32 } from 'node:path';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import { WebSocket } from 'undici';
|
|
6
7
|
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
|
|
@@ -59,6 +60,155 @@ async function fileExists(path) {
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
function remoteDebuggingEndpoint(output) {
|
|
64
|
+
return /DevTools listening on (ws:\/\/[^\s]+)/.exec(output)?.[1] || '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function waitForRemoteDebuggingEndpoint(child, timeoutMs) {
|
|
68
|
+
return new Promise((resolve, reject) => {
|
|
69
|
+
let output = '';
|
|
70
|
+
let settled = false;
|
|
71
|
+
const finish = (error, endpoint) => {
|
|
72
|
+
if (settled) return;
|
|
73
|
+
settled = true;
|
|
74
|
+
clearTimeout(timer);
|
|
75
|
+
child.stdout?.off('data', onData);
|
|
76
|
+
child.stderr?.off('data', onData);
|
|
77
|
+
child.off('error', onError);
|
|
78
|
+
child.off('close', onClose);
|
|
79
|
+
if (error) reject(error);
|
|
80
|
+
else resolve(endpoint);
|
|
81
|
+
};
|
|
82
|
+
const onData = chunk => {
|
|
83
|
+
output = `${output}${chunk}`.slice(-8192);
|
|
84
|
+
const endpoint = remoteDebuggingEndpoint(output);
|
|
85
|
+
if (endpoint) finish(null, endpoint);
|
|
86
|
+
};
|
|
87
|
+
const onError = error => finish(error);
|
|
88
|
+
const onClose = (code, signal) => finish(new Error(`browser exited before exposing DevTools (code ${code ?? 'unknown'}, signal ${signal || 'none'})`));
|
|
89
|
+
const timer = setTimeout(() => finish(new Error(`browser did not expose DevTools within ${timeoutMs}ms`)), timeoutMs);
|
|
90
|
+
child.stdout?.on('data', onData);
|
|
91
|
+
child.stderr?.on('data', onData);
|
|
92
|
+
child.once('error', onError);
|
|
93
|
+
child.once('close', onClose);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function controlBrowser(endpoint, timeoutMs) {
|
|
98
|
+
const url = new URL(endpoint);
|
|
99
|
+
if (url.protocol !== 'ws:' || !['localhost', '127.0.0.1', '::1', '[::1]'].includes(url.hostname)) {
|
|
100
|
+
throw new Error('browser exposed a non-local DevTools endpoint');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
let settled = false;
|
|
105
|
+
const socket = new WebSocket(url.href);
|
|
106
|
+
const finish = error => {
|
|
107
|
+
if (settled) return;
|
|
108
|
+
settled = true;
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
try { socket.close(); } catch {}
|
|
111
|
+
if (error) reject(error);
|
|
112
|
+
else resolve();
|
|
113
|
+
};
|
|
114
|
+
const timer = setTimeout(() => finish(new Error(`DevTools command timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
115
|
+
socket.addEventListener('open', () => socket.send(JSON.stringify({ id: 1, method: 'Browser.getVersion' })));
|
|
116
|
+
socket.addEventListener('message', event => {
|
|
117
|
+
let response;
|
|
118
|
+
try { response = JSON.parse(event.data); }
|
|
119
|
+
catch { return finish(new Error('DevTools returned an invalid response')); }
|
|
120
|
+
if (response.id !== 1) return;
|
|
121
|
+
if (response.error) return finish(new Error(response.error.message || 'DevTools rejected Browser.getVersion'));
|
|
122
|
+
if (!response.result?.product) return finish(new Error('DevTools returned no browser version'));
|
|
123
|
+
finish();
|
|
124
|
+
});
|
|
125
|
+
socket.addEventListener('error', () => finish(new Error('could not connect to the browser DevTools endpoint')));
|
|
126
|
+
socket.addEventListener('close', () => {
|
|
127
|
+
if (!settled) finish(new Error('browser closed the DevTools connection'));
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function waitForClose(child, timeoutMs) {
|
|
133
|
+
return new Promise(resolve => {
|
|
134
|
+
if (child.exitCode !== null || child.signalCode !== null) return resolve(true);
|
|
135
|
+
const timer = setTimeout(() => {
|
|
136
|
+
child.off('close', onClose);
|
|
137
|
+
resolve(false);
|
|
138
|
+
}, timeoutMs);
|
|
139
|
+
const onClose = () => {
|
|
140
|
+
clearTimeout(timer);
|
|
141
|
+
resolve(true);
|
|
142
|
+
};
|
|
143
|
+
child.once('close', onClose);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function signalBrowserTree(child, platform, signal) {
|
|
148
|
+
if (platform === 'win32' && child.pid) {
|
|
149
|
+
try {
|
|
150
|
+
await execFileAsync('taskkill', [
|
|
151
|
+
'/pid', String(child.pid), '/t',
|
|
152
|
+
...(signal === 'SIGKILL' ? ['/f'] : [])
|
|
153
|
+
], { windowsHide: true, timeout: 1000 });
|
|
154
|
+
return;
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
if (platform !== 'win32' && child.pid) process.kill(-child.pid, signal);
|
|
159
|
+
else child.kill(signal);
|
|
160
|
+
} catch {
|
|
161
|
+
try { child.kill(signal); } catch {}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function stopBrowser(child, platform) {
|
|
166
|
+
if (!child || child.exitCode !== null || child.signalCode !== null) return;
|
|
167
|
+
await signalBrowserTree(child, platform, 'SIGTERM');
|
|
168
|
+
if (await waitForClose(child, 1000)) return;
|
|
169
|
+
await signalBrowserTree(child, platform, 'SIGKILL');
|
|
170
|
+
await waitForClose(child, 1000);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function probeSsoBrowser(browser, {
|
|
174
|
+
platform = process.platform,
|
|
175
|
+
timeoutMs = 5000,
|
|
176
|
+
spawnBrowser = spawn,
|
|
177
|
+
controlRemote = controlBrowser
|
|
178
|
+
} = {}) {
|
|
179
|
+
let profile;
|
|
180
|
+
let child;
|
|
181
|
+
try {
|
|
182
|
+
profile = await mkdtemp(join(tmpdir(), 'sap-ai-dev-sso-probe-'));
|
|
183
|
+
child = spawnBrowser(browser.path, [
|
|
184
|
+
`--user-data-dir=${profile}`,
|
|
185
|
+
'--remote-debugging-port=0',
|
|
186
|
+
'--headless=new',
|
|
187
|
+
'--no-first-run',
|
|
188
|
+
'--no-default-browser-check',
|
|
189
|
+
'about:blank'
|
|
190
|
+
], { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: platform !== 'win32' });
|
|
191
|
+
const endpoint = await waitForRemoteDebuggingEndpoint(child, timeoutMs);
|
|
192
|
+
await controlRemote(endpoint, timeoutMs);
|
|
193
|
+
return { usable: true };
|
|
194
|
+
} catch (error) {
|
|
195
|
+
const reason = String(error?.message || error).replace(/\s+/g, ' ').slice(0, 240);
|
|
196
|
+
return { usable: false, reason };
|
|
197
|
+
} finally {
|
|
198
|
+
await stopBrowser(child, platform);
|
|
199
|
+
if (profile) await rm(profile, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function findRemoteControllableSsoBrowsers({ probe = probeSsoBrowser, ...options } = {}) {
|
|
204
|
+
const candidates = await findSsoBrowsers(options);
|
|
205
|
+
const results = await Promise.all(candidates.map(async browser => ({ browser, ...await probe(browser) })));
|
|
206
|
+
return {
|
|
207
|
+
browsers: results.filter(result => result.usable).map(result => result.browser),
|
|
208
|
+
unavailable: results.filter(result => !result.usable).map(({ browser, reason }) => ({ ...browser, reason }))
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
62
212
|
// Every Chromium-based browser the runtime can drive, best first; SAP_BROWSER_EXEC pins one ahead of the rest.
|
|
63
213
|
export async function findSsoBrowsers({ env = process.env, platform = process.platform, exists = fileExists, appPaths = registeredAppPaths } = {}) {
|
|
64
214
|
const candidates = [];
|
package/tools.md
CHANGED
|
@@ -72,11 +72,12 @@ Hidden upstream tools are not directly callable through the proxy. Local workflo
|
|
|
72
72
|
|
|
73
73
|
Broad/destructive surfaces such as object deletion, SQL trace, arbitrary SAP router calls, transport release/delete, RFC execution, debugger listen/delete-breakpoint helpers, and one-off generator/update helpers are intentionally not advertised by the BAS proxy. UI5 repository writes are hidden too: the ADT filestore only reads UI5 apps, so deploy them with SAP Fiori tools instead.
|
|
74
74
|
|
|
75
|
-
##
|
|
75
|
+
## Local and destination-scoped workflow tools
|
|
76
76
|
|
|
77
|
-
The proxy adds local
|
|
77
|
+
The proxy adds local tools to the MCP surface. Some are workspace-wide; others are added to each destination when their required upstream capabilities are available. Like the upstream tools above, they are exposed publicly in lowercase snake_case (`lint_abap`, `lint_abap_project`, `get_application_log`, and so on):
|
|
78
78
|
|
|
79
79
|
- `LintABAP` — local in-memory ABAP linting.
|
|
80
|
+
- `LintABAPProject` — scan serialized ABAP objects in the configured workspace root (SAP_AI_DEV_TOOLKIT_WORKSPACE_ROOT, defaulting to the MCP server working directory) using the project abaplint config and available local dependency folders. This is static analysis only; it does not contact SAP or activate objects.
|
|
80
81
|
- `GetApplicationLog` — bounded convenience mapping to the upstream SAP application-log route when available.
|
|
81
82
|
- `PrepareABAPChangeSet` and `ApplyABAPChangeSet` — review full-source diffs and recheck source before explicit writes.
|
|
82
83
|
- `CheckTransportReadiness` — collect a whitelisted bundle of transport, dependency, inactive-object, ABAP Unit, and ATC results.
|