From 492d8e6e1c7c296708128d9381cf7e63a9077e41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 01:40:34 +0200 Subject: [PATCH 01/12] Point run-debug-bsh.sh at the plugin subproject's build output Moving the plugin into its own Gradle subproject (885ff1a) put compiled classes under plugin/build/, but this script kept looking in build/ at the repository root -- where the root project, a container with no sources of its own, never writes anything. The script could not find BshDebugAgent.class and refused to run. --- plugin/tools/run-debug-bsh.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/tools/run-debug-bsh.sh b/plugin/tools/run-debug-bsh.sh index 9dcbb9b..ec6fd27 100755 --- a/plugin/tools/run-debug-bsh.sh +++ b/plugin/tools/run-debug-bsh.sh @@ -44,8 +44,8 @@ declare -a bshLibMvnCoordinates=( 'org.apache-extras.beanshell' 'bsh' '2.0b6' ) #declare -a bshLibMvnCoordinates=( 'org.beanshell' 'bsh' '2.0b4' ) #declare -a bshLibMvnCoordinates=( 'bsh' 'bsh' '2.0b1' ) -# BshDebugAgent, compiled into `build/` by `./gradlew :plugin:compileJava`. -declare agent_classes_dir="${repo}/build/classes/java/main" +# BshDebugAgent, compiled into `plugin/build/` by `./gradlew :plugin:compileJava`. +declare agent_classes_dir="${repo}/plugin/build/classes/java/main" declare agent_class='cz.loplex.intellij.bsh.debug.agent.BshDebugAgent' From 779579f867304fc16dbdc8f57faea47d9e199246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 01:41:25 +0200 Subject: [PATCH 02/12] Exit non-zero from run-debug-bsh.sh on a failing script bsh.Interpreter's own main() catches a script's EvalError/TargetError, prints it and returns -- never a non-zero exit, so a CI caller could not tell a failing script from a successful one. Since main() itself swallows the exception, the fix has to sit above bsh.Interpreter rather than in it: BshRunner makes the same source() call main() does, but turns that same exception into exit(1). --- docs/FUTURE_WORK.md | 9 --------- plugin/tools/run-debug-bsh.sh | 28 +++++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index 5cf705b..75329b6 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -101,15 +101,6 @@ debuggee itself. ## Smaller, independent -### Non-zero exit from `tools/run-debug-bsh.sh` on script errors - -`bsh.Interpreter` prints a "Target exception" but still exits `0` when a script -fails to evaluate (the connect-failure case is already handled — the agent calls -`System.exit(69)`). For the command-line tools it would be nicer if a failing -script produced a non-zero exit, so callers/CI can detect it. A wrapper-level -concern (parse the interpreter's output, or run the script via a small launcher -that propagates eval errors), not an agent change. - ### (Optional) Second JDWP channel for step-into Java The original inline-debug plan left one optional item unimplemented: a second, diff --git a/plugin/tools/run-debug-bsh.sh b/plugin/tools/run-debug-bsh.sh index ec6fd27..4dc35ac 100755 --- a/plugin/tools/run-debug-bsh.sh +++ b/plugin/tools/run-debug-bsh.sh @@ -93,10 +93,36 @@ fi # Create temporary file where to put the instrumented script, kept after the run for possible inspection. instrumented=$( mktemp --suffix='.bsh' ) +runner_dir=$( dirname "${instrumented}" ) # Instrument input script on STDIN and writes the enriched script to temporary file. dbgExec "${here}/bshInstrumenter.main.kts" > "${instrumented}" echo "Instrumented script: ${instrumented}" >&2 +# bsh.Interpreter's own main() catches a failing script's EvalError/TargetError, prints it and +# returns -- it never exits non-zero, so a caller (CI included) cannot tell success from failure. +# BshRunner makes the same source() call main() does but turns that same exception into exit(1). +cat > "${runner_dir}/BshRunner.java" <<'EOF' +import bsh.EvalError; +import bsh.Interpreter; +import bsh.TargetError; + +public final class BshRunner { + public static void main(String[] args) throws Exception { + Interpreter interpreter = new Interpreter(); + try { + interpreter.source(args[0], interpreter.getNameSpace()); + } catch (TargetError e) { + System.err.println("Script threw exception: " + e); + System.exit(1); + } catch (EvalError e) { + System.err.println("Evaluation Error: " + e); + System.exit(1); + } + } +} +EOF +dbgExec javac -cp "${bsh_jar}" -d "${runner_dir}" "${runner_dir}/BshRunner.java" + # Run instrumented script in BeanShell interpreter with BshDebugAgent on classpath. -dbgExec exec java -cp "${agent_classes_dir}:${bsh_jar}" "${portArgs[@]}" 'bsh.Interpreter' "${instrumented}" +dbgExec exec java -cp "${agent_classes_dir}:${bsh_jar}:${runner_dir}" "${portArgs[@]}" 'BshRunner' "${instrumented}" From fb69b465790b54350925281265d36d9da6053642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 02:16:59 +0200 Subject: [PATCH 03/12] Record the VS Code GUI test idea in FUTURE_WORK.md Discussed and deliberately deferred yesterday, but only written down in Claude's own memory rather than the repo -- so it would have been lost to anyone (or any session) not carrying that memory. FUTURE_WORK.md is the actual parking lot. --- docs/FUTURE_WORK.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index 75329b6..a0758eb 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -107,3 +107,13 @@ The original inline-debug plan left one optional item unimplemented: a second, JDWP-based debug channel (reuse `BshJavaDebugAttach`) so the developer can step *into* the Java code a Maven-run script calls, in addition to line-stepping the script itself. Independent of the script-level transport; purely additive. + +### End-to-end GUI test for the VS Code extension + +`editors/vscode/` has no automated test of its own yet — only the agent-side +`agent/checks/07-dap-transport.sh`, which exercises `DapChannel` but never opens +VS Code itself. Cover it with `@vscode/test-electron` (Mocha): launch a real, +headless VS Code (Xvfb) against a workspace containing a `.bsh` file, call +`vscode.debug.startDebugging(...)`, and assert on the DAP traffic via a +`DebugAdapterTracker` — the same shape as the existing agent check, one layer up +the stack. From 98632b4b1d3094c4d9ecf0990dffacbbbe024e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 12:37:21 +0200 Subject: [PATCH 04/12] Add an end-to-end GUI test for the VS Code extension Drives a real, headless VS Code (@vscode/test-electron + Mocha) against the fixture in src/test/fixtures/workspace/, starting an actual "launch" session so the test reaches BshDebugAdapterDescriptorFactory.launch() -- the one thing agent/checks/07-dap-transport.sh, which only ever attaches to a JVM it already started, cannot cover. Reuses 07's own fixture, breakpoint line and evaluate expression so both checks are provably exercising the same behaviour one layer apart, and drives stackTrace/scopes/variables/evaluate/continue through session.customRequest() since no UI is present to trigger them by clicking. Two things only running it against a real client turned up. DapChannel never sends a terminated/exited DAP event -- the JVM exiting just drops the socket -- so completion is detected via onDidTerminateDebugSession instead, the same signal descriptorFactory.ts already uses to know when to stop waiting on the child process. And making it actually headless took two tries. Electron's Ozone platform prefers a real Wayland compositor over the X11 display xvfb-run sets up whenever one is reachable, which would have put the supposedly headless test window on screen; an ELECTRON_OZONE_PLATFORM_HINT=x11 env var alone was not enough to stop it, since xvfb-run leaves WAYLAND_DISPLAY itself in place for Electron's own auto-detection to find. runTest.ts removes WAYLAND_DISPLAY from the child's environment entirely and passes --ozone-platform=x11 as a hard switch instead, leaving Electron nothing to prefer over X11 -- verified by polling `xdotool search` against the real display while the test ran. Documented in docs/FUTURE_WORK.md and editors/vscode/README.md. --- docs/FUTURE_WORK.md | 48 +- editors/vscode/.gitignore | 1 + editors/vscode/.vscodeignore | 1 + editors/vscode/README.md | 23 + editors/vscode/package-lock.json | 1446 ++++++++++++++++- editors/vscode/package.json | 7 +- .../src/test/fixtures/workspace/script.bsh | 11 + editors/vscode/src/test/runTest.ts | 58 + editors/vscode/src/test/suite/debug.test.ts | 132 ++ editors/vscode/src/test/suite/index.ts | 17 + editors/vscode/tsconfig.json | 1 + 11 files changed, 1730 insertions(+), 15 deletions(-) create mode 100644 editors/vscode/src/test/fixtures/workspace/script.bsh create mode 100644 editors/vscode/src/test/runTest.ts create mode 100644 editors/vscode/src/test/suite/debug.test.ts create mode 100644 editors/vscode/src/test/suite/index.ts diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index a0758eb..aa5f021 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -108,12 +108,42 @@ JDWP-based debug channel (reuse `BshJavaDebugAttach`) so the developer can step *into* the Java code a Maven-run script calls, in addition to line-stepping the script itself. Independent of the script-level transport; purely additive. -### End-to-end GUI test for the VS Code extension - -`editors/vscode/` has no automated test of its own yet — only the agent-side -`agent/checks/07-dap-transport.sh`, which exercises `DapChannel` but never opens -VS Code itself. Cover it with `@vscode/test-electron` (Mocha): launch a real, -headless VS Code (Xvfb) against a workspace containing a `.bsh` file, call -`vscode.debug.startDebugging(...)`, and assert on the DAP traffic via a -`DebugAdapterTracker` — the same shape as the existing agent check, one layer up -the stack. +### End-to-end GUI test for the VS Code extension — done + +`editors/vscode/src/test/` now covers what `agent/checks/07-dap-transport.sh` cannot: a real, +headless VS Code (`@vscode/test-electron` + Mocha, under Xvfb) starting an actual `launch` +session against the fixture in `src/test/fixtures/workspace/`, asserting on the DAP traffic via +a `DebugAdapterTracker`. The fixture, breakpoint line and evaluate expression are the same ones +`07` already proved work over `DapChannel` — deliberately, so the two checks are provably +exercising the same behaviour one layer apart rather than two fixtures that could quietly drift. + +**The one real gap `07` cannot reach**, and the reason this is `launch` rather than `attach`: +`dap-client.py` connects to a JVM the check already started, so it never touches this +extension's own `BshDebugAdapterDescriptorFactory.launch()` — the port allocation, the +`-javaagent` spawn, watching stdout for `DAP: listening`. `attach` would have covered `DapChannel` +a second time and nothing new. + +**The handshake needed nothing hand-rolled.** `dap-client.py` drives `initialize` and +`configurationDone` itself because it *is* the client; `vscode.debug.startDebugging()` does that +internally, so the test only needed `session.customRequest()` for what a UI would otherwise +trigger by clicking — `stackTrace`, `scopes`, `variables`, `evaluate`, `continue`. + +**What running it against a real client actually found**, and would not have shown up against +`dap-client.py`: `DapChannel` never sends a `terminated` or `exited` DAP event. When the script +runs to completion the JVM just exits and the socket drops, and `dap-client.py` never noticed +because it only speaks the protocol, not VS Code's own session bookkeeping. A real client has +to fall back to `onDidTerminateDebugSession` — the same signal `descriptorFactory.ts` already +uses to know when to stop waiting on the child process — rather than a message on the wire. Not +a bug to fix here, but worth knowing before adding another DAP client: nothing announces normal +completion, only the connection going away. + +**"Headless" needed an extra push on a real Wayland desktop.** `xvfb-run` sets `DISPLAY` for its +virtual framebuffer, but leaves `WAYLAND_DISPLAY` alone, and Electron's Ozone platform selection +prefers a real Wayland compositor over that X11 display whenever one is reachable -- an +`ELECTRON_OZONE_PLATFORM_HINT=x11` env var was not enough to stop it. `runTest.ts` removes +`WAYLAND_DISPLAY` from the child's environment outright and passes `--ozone-platform=x11` as a +hard switch, so there is nothing left for Electron to prefer. + +Run with `npm test` (`xvfb-run -a npm test` headless); resolves `AGENT_JAR`/`BSH_CLASSPATH` +through the same `:agent:samples:printPaths` Gradle task `agent/checks/lib.sh` uses. Documented +in [`editors/vscode/README.md`](../editors/vscode/README.md#testing). diff --git a/editors/vscode/.gitignore b/editors/vscode/.gitignore index c92a7d3..c3bfe58 100644 --- a/editors/vscode/.gitignore +++ b/editors/vscode/.gitignore @@ -1,3 +1,4 @@ out/ node_modules/ *.vsix +.vscode-test/ diff --git a/editors/vscode/.vscodeignore b/editors/vscode/.vscodeignore index 41e2bec..543206d 100644 --- a/editors/vscode/.vscodeignore +++ b/editors/vscode/.vscodeignore @@ -1,5 +1,6 @@ .vscode/** src/** +out/test/** .gitignore tsconfig.json **/*.map diff --git a/editors/vscode/README.md b/editors/vscode/README.md index 17370da..8626ef0 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -72,6 +72,29 @@ Stopping the session (rather than disconnecting) kills the JVM this extension la *attached* session leaves the target process alone either way, since it was never this extension's to manage. +## Testing + +`src/test/` drives a real, headless VS Code (`@vscode/test-electron` + Mocha) against the fixture +workspace in `src/test/fixtures/workspace/` -- the same script, breakpoint line and evaluate +expression [`agent/checks/07-dap-transport.sh`](../../agent/checks/07-dap-transport.sh) already +proves work over `DapChannel`, driven here through a real `launch` session instead of the +standalone `dap-client.py`, so it covers the one thing `07` cannot: this extension's own +`BshDebugAdapterDescriptorFactory.launch()` (port allocation, the `-javaagent` spawn, the +`DAP: listening` stdout watch). Assertions run against the DAP traffic via a +`DebugAdapterTracker` and `session.customRequest()`, since no UI is present to trigger +`stackTrace`/`scopes`/`variables`/`evaluate` by clicking. Completion is detected via +`onDidTerminateDebugSession` rather than a `terminated` DAP event, since `DapChannel` never +sends one -- the JVM exiting just drops the socket. + +```bash +npm test # needs a display +xvfb-run -a npm test # headless / CI +``` + +`npm test` builds the agent jar itself, via the same `:agent:samples:printPaths` Gradle task +[`agent/checks/lib.sh`](../../agent/checks/lib.sh) uses. The first run also downloads and caches +a VS Code build under `.vscode-test/`. + ## Alternatives [`../neovim/`](../neovim/) and [`../eclipse/`](../eclipse/) cover the same transport for those diff --git a/editors/vscode/package-lock.json b/editors/vscode/package-lock.json index f09e379..e161408 100644 --- a/editors/vscode/package-lock.json +++ b/editors/vscode/package-lock.json @@ -9,14 +9,24 @@ "version": "0.1.0", "license": "Apache-2.0", "devDependencies": { + "@types/mocha": "^10.0.0", "@types/node": "^20.0.0", "@types/vscode": "^1.90.0", + "@vscode/test-electron": "^2.4.0", + "mocha": "^10.0.0", "typescript": "^5.4.0" }, "engines": { "vscode": "^1.90.0" } }, + "node_modules/@types/mocha": { + "version": "10.0.10", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", + "integrity": "sha512-xPyYSz1cMPnJQhl0CLMH68j3gprKZaTjG3s5Vi+fDgx+uhG9NOXwbVt52eFS8ECyXhyKcjDLCBEqBExKuiZb7Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", @@ -24,16 +34,1245 @@ "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.125.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", + "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vscode/test-electron": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", + "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.3.tgz", + "integrity": "sha512-DRdx5neNsG/QXbniLFWi2YmC/68oeOOmKz6zOjVk6ZS1ZLXgLIKqVEc6hWsmkjBbgii0SwaBTcJ5XKj5gzY/4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true, + "license": "ISC" + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cliui/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.8.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.8.2.tgz", + "integrity": "sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^8.1.0", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^16.2.0", + "yargs-parser": "^20.2.9", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@types/vscode": { - "version": "1.125.0", - "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.125.0.tgz", - "integrity": "sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==", + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", "dev": true, "license": "MIT" }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -54,6 +1293,203 @@ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/editors/vscode/package.json b/editors/vscode/package.json index a162daa..a32b701 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -173,11 +173,16 @@ "scripts": { "compile": "tsc -p ./", "watch": "tsc -w -p ./", - "vscode:prepublish": "npm run compile" + "vscode:prepublish": "npm run compile", + "pretest": "npm run compile", + "test": "node ./out/test/runTest.js" }, "devDependencies": { + "@types/mocha": "^10.0.0", "@types/node": "^20.0.0", "@types/vscode": "^1.90.0", + "@vscode/test-electron": "^2.4.0", + "mocha": "^10.0.0", "typescript": "^5.4.0" } } diff --git a/editors/vscode/src/test/fixtures/workspace/script.bsh b/editors/vscode/src/test/fixtures/workspace/script.bsh new file mode 100644 index 0000000..1c52644 --- /dev/null +++ b/editors/vscode/src/test/fixtures/workspace/script.bsh @@ -0,0 +1,11 @@ +// Mirrors the fixture agent/checks/07-dap-transport.sh builds inline, so the same +// breakpoint line and evaluate expression are proven to work over DapChannel before +// this suite drives them through a real VS Code debug session. +total = 0; +compute(n) { + doubled = n * 2; + return doubled + total; +} +total = 5; +print("result=" + compute(7)); +print("script done"); diff --git a/editors/vscode/src/test/runTest.ts b/editors/vscode/src/test/runTest.ts new file mode 100644 index 0000000..85689e0 --- /dev/null +++ b/editors/vscode/src/test/runTest.ts @@ -0,0 +1,58 @@ +import * as cp from 'child_process'; +import * as path from 'path'; +import { runTests } from '@vscode/test-electron'; + +/** + * Resolves AGENT_JAR and BSH_CLASSPATH the same way agent/checks/lib.sh's need_paths() does: + * by asking Gradle, since the BeanShell coordinates live in the version catalog and the agent + * jar sits under a content hash -- any path guessed here would be wrong the moment either changed. + */ +function resolveAgentPaths(repoRoot: string): { agentJar: string; classpath: string } { + const gradlew = path.join(repoRoot, process.platform === 'win32' ? 'gradlew.bat' : 'gradlew'); + const output = cp.execFileSync( + gradlew, + ['-q', '-p', repoRoot, ':agent:samples:printPaths'], + { encoding: 'utf8' } + ); + + const classpath = /^BSH_CLASSPATH=(.*)$/m.exec(output)?.[1]; + const agentJar = /^AGENT_JAR=(.*)$/m.exec(output)?.[1]; + if (!classpath || !agentJar) { + throw new Error(`could not parse ':agent:samples:printPaths' output:\n${output}`); + } + return { agentJar, classpath }; +} + +async function main(): Promise { + const extensionDevelopmentPath = path.resolve(__dirname, '..', '..'); + const extensionTestsPath = path.resolve(__dirname, 'suite', 'index'); + const repoRoot = path.resolve(extensionDevelopmentPath, '..', '..'); + + // Fixtures are plain data, not compiled sources, so they are read from src/ rather than out/. + const workspacePath = path.join(extensionDevelopmentPath, 'src', 'test', 'fixtures', 'workspace'); + + const { agentJar, classpath } = resolveAgentPaths(repoRoot); + + // On a real Wayland desktop, Electron's Ozone platform selection prefers the real + // compositor over the X11 DISPLAY xvfb-run sets up -- an env hint alone was not enough to + // stop it, so WAYLAND_DISPLAY is removed outright: with no Wayland socket to find, there is + // nothing left for Electron to prefer over X11. + const testEnv: NodeJS.ProcessEnv = { + ...process.env, + BSH_AGENT_JAR: agentJar, + BSH_CLASSPATH: classpath, + }; + delete testEnv.WAYLAND_DISPLAY; + + await runTests({ + extensionDevelopmentPath, + extensionTestsPath, + launchArgs: [workspacePath, '--disable-extensions', '--ozone-platform=x11'], + extensionTestsEnv: testEnv, + }); +} + +main().catch((err) => { + console.error('Failed to run the VS Code extension tests:', err); + process.exit(1); +}); diff --git a/editors/vscode/src/test/suite/debug.test.ts b/editors/vscode/src/test/suite/debug.test.ts new file mode 100644 index 0000000..38da931 --- /dev/null +++ b/editors/vscode/src/test/suite/debug.test.ts @@ -0,0 +1,132 @@ +import * as assert from 'assert'; +import { EventEmitter } from 'events'; +import * as vscode from 'vscode'; + +// Same fixture, same breakpoint line and evaluate expression as +// agent/checks/07-dap-transport.sh, one layer up the stack: this drives them through a real +// VS Code debug session instead of the standalone dap-client.py. +const FIXTURE_SCRIPT = 'script.bsh'; +const BREAKPOINT_LINE = 7; // `return doubled + total;` + +function waitFor(events: EventEmitter, event: string): Promise { + return new Promise((resolve) => events.once(event, resolve)); +} + +suite('BeanShell debug adapter (VS Code)', function () { + this.timeout(60_000); + + test('launches the agent, hits a breakpoint, evaluates, and runs to completion', async () => { + const agentJar = process.env.BSH_AGENT_JAR; + const classpath = process.env.BSH_CLASSPATH; + assert.ok(agentJar, 'BSH_AGENT_JAR must be set by runTest.ts'); + assert.ok(classpath, 'BSH_CLASSPATH must be set by runTest.ts'); + + const folder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(folder, 'expected the fixture workspace to be open'); + const scriptUri = vscode.Uri.joinPath(folder.uri, FIXTURE_SCRIPT); + + // The adapter never fires the initialize/configurationDone handshake by hand here -- + // vscode.debug.startDebugging() drives that internally. What is worth watching is + // everything a UI would otherwise trigger by clicking: stackTrace, scopes, variables, + // evaluate, continue -- so those go through session.customRequest() below, exactly as + // dap-client.py drives them explicitly against the raw socket. + const events = new EventEmitter(); + const trackerDisposable = vscode.debug.registerDebugAdapterTrackerFactory('bsh', { + createDebugAdapterTracker(): vscode.DebugAdapterTracker { + return { + onDidSendMessage(message: any) { + if (message.type === 'event') { + events.emit(message.event, message); + } + }, + }; + }, + }); + + try { + vscode.debug.addBreakpoints([ + new vscode.SourceBreakpoint( + new vscode.Location(scriptUri, new vscode.Position(BREAKPOINT_LINE - 1, 0)) + ), + ]); + + let stopped = waitFor(events, 'stopped'); + const started = await vscode.debug.startDebugging(folder, { + type: 'bsh', + request: 'launch', + name: 'e2e', + script: scriptUri.fsPath, + agentJar, + classpath, + }); + assert.ok(started, 'startDebugging did not start a session'); + + const session = vscode.debug.activeDebugSession; + assert.ok(session, 'expected an active debug session'); + + // Mirrors agent/checks/07-dap-transport.sh: the first stop is the script's own + // first statement (reported before the agent could know any breakpoints existed), + // not yet inside compute() -- so this rides out stops until one actually lands in + // compute(), the same way dap-client.py's --stops loop does, rather than assuming + // the first "stopped" event is the breakpoint. + let threadId: number; + let frameNames: string[]; + let stack: any; + for (let attempt = 0; ; attempt++) { + assert.ok(attempt < 4, 'never reached a stop inside compute()'); + const stoppedMessage = await stopped; + // DapChannel deliberately sends the same generic "pause" for every stop -- + // it does not distinguish "breakpoint" from "step" -- so that is what a real + // client sees here too, not "breakpoint". + assert.strictEqual(stoppedMessage.body.reason, 'pause'); + threadId = stoppedMessage.body.threadId; + + stack = await session.customRequest('stackTrace', { threadId }); + frameNames = stack.stackFrames.map((f: any) => f.name); + if (frameNames.includes('compute')) { + break; + } + stopped = waitFor(events, 'stopped'); + await session.customRequest('continue', { threadId }); + } + assert.ok(frameNames.length >= 2, 'expected the caller frame in the stack too'); + + const topFrameId = stack.stackFrames[0].id; + const scopes = await session.customRequest('scopes', { frameId: topFrameId }); + const scopeNames = scopes.scopes.map((s: any) => s.name); + assert.ok(scopeNames.includes('Locals'), `expected a Locals scope, got: ${scopeNames}`); + assert.ok(scopeNames.includes('Global'), `expected a Global scope, got: ${scopeNames}`); + + const localsScope = scopes.scopes.find((s: any) => s.name === 'Locals'); + const variables = await session.customRequest('variables', { + variablesReference: localsScope.variablesReference, + }); + const doubled = variables.variables.find((v: any) => v.name === 'doubled'); + assert.strictEqual(doubled?.value, '14'); + + const evaluated = await session.customRequest('evaluate', { + expression: 'doubled + 1', + frameId: topFrameId, + }); + assert.strictEqual(evaluated.result, '15'); + + // DapChannel never sends a "terminated"/"exited" DAP event -- the JVM just exits and + // the socket drops once the script runs to completion, so a real client only learns + // the session is over the way VS Code itself does: onDidTerminateDebugSession, not a + // message on the wire. + const ended = new Promise((resolve) => { + const sub = vscode.debug.onDidTerminateDebugSession((endedSession) => { + if (endedSession.id === session.id) { + sub.dispose(); + resolve(); + } + }); + }); + await session.customRequest('continue', { threadId }); + await ended; + } finally { + trackerDisposable.dispose(); + vscode.debug.removeBreakpoints(vscode.debug.breakpoints); + } + }); +}); diff --git a/editors/vscode/src/test/suite/index.ts b/editors/vscode/src/test/suite/index.ts new file mode 100644 index 0000000..6d56c9a --- /dev/null +++ b/editors/vscode/src/test/suite/index.ts @@ -0,0 +1,17 @@ +import * as path from 'path'; +import Mocha from 'mocha'; + +export async function run(): Promise { + const mocha = new Mocha({ ui: 'tdd', color: true, timeout: 60_000 }); + mocha.addFile(path.join(__dirname, 'debug.test.js')); + + return new Promise((resolve, reject) => { + mocha.run((failures: number) => { + if (failures > 0) { + reject(new Error(`${failures} test(s) failed.`)); + } else { + resolve(); + } + }); + }); +} diff --git a/editors/vscode/tsconfig.json b/editors/vscode/tsconfig.json index 31dd686..3ba4d38 100644 --- a/editors/vscode/tsconfig.json +++ b/editors/vscode/tsconfig.json @@ -9,6 +9,7 @@ "rootDir": "src", "sourceMap": true, "strict": true, + "esModuleInterop": true, "noUnusedLocals": true, "noUnusedParameters": true, "moduleResolution": "node" From 0844409a51a9e9284853c02e8ca174b5cc8e5f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 03:09:32 +0200 Subject: [PATCH 05/12] Document that the Maven path already gets Java step-into for free FUTURE_WORK.md listed a second JDWP channel for the Maven path as unimplemented optional work, mirroring the standalone .bsh path's manual BshJavaDebugAttach wiring. Verified by hand in runIde that this was never needed: because BshMavenRunConfiguration only augments getState() before delegating to MavenRunConfiguration's own, the Debug executor already wraps the forked Maven JVM in JDWP like any other Maven run, and a breakpoint in Java code the script calls into is actually hit. DEBUGGING.md's dual-session section was scoped to BshDebugRunner only; note the Maven path's very different (free) route to the same result. --- docs/FUTURE_WORK.md | 26 ++++++++++++++++++++------ plugin/docs/DEBUGGING.md | 8 ++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index aa5f021..572bdcf 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -101,12 +101,26 @@ debuggee itself. ## Smaller, independent -### (Optional) Second JDWP channel for step-into Java - -The original inline-debug plan left one optional item unimplemented: a second, -JDWP-based debug channel (reuse `BshJavaDebugAttach`) so the developer can step -*into* the Java code a Maven-run script calls, in addition to line-stepping the -script itself. Independent of the script-level transport; purely additive. +### Second JDWP channel for step-into Java — already works, no code needed + +The original inline-debug plan left this as an optional item: a second, JDWP-based +debug channel (reuse `BshJavaDebugAttach`) so the developer can step *into* the Java +code a Maven-run script calls, in addition to line-stepping the script itself. + +It turns out there is nothing to build. `BshMavenRunConfiguration` extends +`MavenRunConfiguration` and only augments `getState()` before delegating to the +super implementation, so the Debug executor wraps the forked Maven JVM in JDWP the +same way it would for any Maven run — the Java debug tab "appears for free", as the +class doc on `BshMavenRunConfiguration` already said. Verified by hand on +2026-07-29 in `./gradlew :plugin:runIde`: running `plugin/samples/maven/build-helper`'s +`bsh-property` goal under Debug opened both a `BeanShell (Maven)` session and a +`build-helper [install] (bsh)` Java session, and a breakpoint in `java.lang.String.length()` +(reached from the inline script's `project.getVersion().length()`) was actually hit, +not just a tab that appeared and did nothing. + +`BshJavaDebugAttach` and the manual `-agentlib:jdwp` wiring in `BshDebugRunner.kt` +remain necessary for the standalone `.bsh` path, which runs a raw `GeneralCommandLine` +outside the platform's own Java-debugging support and so gets nothing "for free". ### End-to-end GUI test for the VS Code extension — done diff --git a/plugin/docs/DEBUGGING.md b/plugin/docs/DEBUGGING.md index 3953b07..a9ae7fd 100644 --- a/plugin/docs/DEBUGGING.md +++ b/plugin/docs/DEBUGGING.md @@ -138,6 +138,14 @@ breakpoints, script variables) and a **Java** session (breakpoints in the Java code the script invokes). Without the Java plugin, JDWP is not added and only the BeanShell debugger runs. +**The Maven path gets this for free, with none of the above.** `BshMavenRunConfiguration` +extends `MavenRunConfiguration` and only augments `getState()` before delegating to +`super.getState()`, so the Debug executor wraps the forked Maven JVM in JDWP exactly as +it would for any other Maven run — no `BshJavaDebugAttach`, no manual `-agentlib:jdwp`. +Verified by hand: debugging `plugin/samples/maven/build-helper`'s `bsh-property` goal +opens a `BeanShell (Maven)` session alongside a Java one, and a breakpoint in Java code +the script calls into is actually hit. See [`FUTURE_WORK.md`](../../docs/FUTURE_WORK.md). + ## Variables and frames Both are fetched on demand. A stop reports only the stack — one entry per frame, From c6fae7aea27d07ad92b001710c7ea63c7b173d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 09:02:12 +0200 Subject: [PATCH 06/12] Record Neovim/Eclipse e2e tests and GitHub Actions as future work The VS Code extension just got a real end-to-end GUI test; Neovim and Eclipse still only have the manual dap-client.py coverage, which isn't a real client. Also note that there is no CI yet, even though the Gradle build and the agent checks need nothing exotic to run on a hosted runner. --- docs/FUTURE_WORK.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index 572bdcf..a47f158 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -161,3 +161,28 @@ hard switch, so there is nothing left for Electron to prefer. Run with `npm test` (`xvfb-run -a npm test` headless); resolves `AGENT_JAR`/`BSH_CLASSPATH` through the same `:agent:samples:printPaths` Gradle task `agent/checks/lib.sh` uses. Documented in [`editors/vscode/README.md`](../editors/vscode/README.md#testing). + +### End-to-end GUI tests for Neovim and Eclipse + +VS Code has one now (above); Neovim and Eclipse still only get the manual +`agent/checks/07-dap-transport.sh` / `dap-client.py` coverage, which speaks the protocol but +is not a real client. That distinction is exactly what made the VS Code test worth writing: +its one real finding — `DapChannel` never sends a `terminated`/`exited` DAP event — came from +using `vscode.debug.startDebugging()` instead of a hand-rolled script, and there is no reason +the same class of gap couldn't exist in either editor's own session bookkeeping instead. + +Neovim is the more tractable of the two: `nvim --headless` plus a Lua test runner (`plenary.nvim` +or a plain `nvim -l` script) can drive `nvim-dap` the same way the VS Code test drives +`vscode.debug.startDebugging()`, with no display server needed. Eclipse's generic DAP client has +no headless, scriptable entry point equivalent to that call — an end-to-end test there would mean +SWTBot or similar driving real windows, for less differentiated coverage, since +[`editors/eclipse/`](../editors/eclipse/README.md) is already attach-only and so exercises less +of the extension-specific code the VS Code test caught its bug in. + +### GitHub Actions for builds + +No CI yet — `./gradlew build` and `agent/checks/run-all.sh` only run when someone remembers to +run them locally. Both need nothing exotic: JDK 17+ (21 to match what Kotlin compiles to), and +for the checks, `mvn` and `python3` — all present on GitHub's `ubuntu-latest` image. The VS Code +extension's own `npm test` (Xvfb + Electron) is a separate job at least, since it needs a virtual +framebuffer and the Wayland-avoidance fix already in `runTest.ts`. From 5707450d7d45d1a03a5dd8ef38b95e441cd9e0c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 09:02:53 +0200 Subject: [PATCH 07/12] Add a GitHub Actions workflow for the Gradle build and agent checks Two jobs, mirroring the split agent/checks/README.md already explains: a plain ./gradlew build, and agent/checks/run-all.sh, which exercises what a Gradle test cannot arrange from inside itself (a real mvn process, a -javaagent JVM, a socket between two processes). Both run on ubuntu-latest with nothing extra installed -- mvn and python3 are already on the image. Verified locally on JDK 21 before committing: the build and all 7 checks pass. Marks the corresponding FUTURE_WORK.md item done. --- .github/workflows/ci.yml | 41 ++++++++++++++++++++++++++++++++++++++++ docs/FUTURE_WORK.md | 12 ++++++------ 2 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f4d904b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + gradle-build: + name: Gradle build (plugin + agent) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + cache: gradle + + - name: Build and test + run: ./gradlew build --stacktrace + + agent-checks: + name: Agent end-to-end checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + cache: gradle + + # ubuntu-latest already ships Maven and Python 3, which the checks need + # (02-maven-plugin-realm.sh, dap-client.py); nothing extra to install. + - name: Run agent/checks + run: ./agent/checks/run-all.sh diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index a47f158..073b24f 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -179,10 +179,10 @@ SWTBot or similar driving real windows, for less differentiated coverage, since [`editors/eclipse/`](../editors/eclipse/README.md) is already attach-only and so exercises less of the extension-specific code the VS Code test caught its bug in. -### GitHub Actions for builds +### GitHub Actions for builds — done -No CI yet — `./gradlew build` and `agent/checks/run-all.sh` only run when someone remembers to -run them locally. Both need nothing exotic: JDK 17+ (21 to match what Kotlin compiles to), and -for the checks, `mvn` and `python3` — all present on GitHub's `ubuntu-latest` image. The VS Code -extension's own `npm test` (Xvfb + Electron) is a separate job at least, since it needs a virtual -framebuffer and the Wayland-avoidance fix already in `runTest.ts`. +`.github/workflows/ci.yml` runs `./gradlew build` and `agent/checks/run-all.sh` on every push and +pull request, on `ubuntu-latest` with JDK 21 — nothing exotic needed since `mvn` and `python3` are +already on the image. The VS Code and Neovim extension tests (Xvfb + Electron, and `nvim -l`) are +not wired into CI yet — each needs its own runner setup (a display for the former, `nvim` and +`git` on `PATH` for the latter) and is left for a follow-up job. From b558bdc915f76575a02d507a2b8756af708a275f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 09:27:26 +0200 Subject: [PATCH 08/12] Add an end-to-end GUI test for the Neovim DAP wiring editors/neovim/tests/ drives bsh-dap.lua through a real, headless nvim-dap session (nvim -l, no display server) against the same fixture, breakpoint line and evaluate expression agent/checks/07-dap-transport.sh and the VS Code test already prove work over DapChannel -- so it covers what dap-client.py cannot: bsh-dap.lua's own launch(), the jobstart spawn and the "DAP: listening" stdout watch. nvim-dap is fetched into tests/.deps/ (gitignored) pinned to a fixed commit, since this test exercises bsh-dap.lua through it rather than vendoring its code. Confirms, on the Neovim side, the same finding the VS Code test made: DapChannel never sends a terminated/exited DAP event, so both clients only learn a session ended from the socket dropping. Marks the corresponding FUTURE_WORK.md item done, and splits Eclipse's off into its own entry now that it's the only one left open. --- docs/FUTURE_WORK.md | 48 ++++++--- editors/neovim/README.md | 24 +++++ editors/neovim/tests/.gitignore | 1 + editors/neovim/tests/fixtures/script.bsh | 10 ++ editors/neovim/tests/run-tests.sh | 49 +++++++++ editors/neovim/tests/run.lua | 126 +++++++++++++++++++++++ 6 files changed, 244 insertions(+), 14 deletions(-) create mode 100644 editors/neovim/tests/.gitignore create mode 100644 editors/neovim/tests/fixtures/script.bsh create mode 100755 editors/neovim/tests/run-tests.sh create mode 100644 editors/neovim/tests/run.lua diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index 073b24f..4dbb8a0 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -162,22 +162,42 @@ Run with `npm test` (`xvfb-run -a npm test` headless); resolves `AGENT_JAR`/`BSH through the same `:agent:samples:printPaths` Gradle task `agent/checks/lib.sh` uses. Documented in [`editors/vscode/README.md`](../editors/vscode/README.md#testing). -### End-to-end GUI tests for Neovim and Eclipse - -VS Code has one now (above); Neovim and Eclipse still only get the manual -`agent/checks/07-dap-transport.sh` / `dap-client.py` coverage, which speaks the protocol but -is not a real client. That distinction is exactly what made the VS Code test worth writing: -its one real finding — `DapChannel` never sends a `terminated`/`exited` DAP event — came from -using `vscode.debug.startDebugging()` instead of a hand-rolled script, and there is no reason -the same class of gap couldn't exist in either editor's own session bookkeeping instead. - -Neovim is the more tractable of the two: `nvim --headless` plus a Lua test runner (`plenary.nvim` -or a plain `nvim -l` script) can drive `nvim-dap` the same way the VS Code test drives -`vscode.debug.startDebugging()`, with no display server needed. Eclipse's generic DAP client has -no headless, scriptable entry point equivalent to that call — an end-to-end test there would mean +### End-to-end GUI test for Neovim — done + +`editors/neovim/tests/` drives `bsh-dap.lua` through a real, headless `nvim-dap` session +(`nvim --headless -l`, no display server needed) against the same fixture, breakpoint line and +evaluate expression `agent/checks/07-dap-transport.sh` and the VS Code test already prove work +over `DapChannel` — deliberately, for the same reason the VS Code fixture matches `07`'s: so the +three checks are provably exercising the same behaviour, not three fixtures that could drift. +Covers what `07`'s `dap-client.py` cannot: `bsh-dap.lua`'s own `launch()` (the `jobstart` spawn, +the `DAP: listening` stdout watch), the Neovim counterpart to what the VS Code GUI test found for +`BshDebugAdapterDescriptorFactory.launch()`. + +**No test framework needed.** `nvim -l` (a Lua-script entry point, not `-c`/`-u` sourcing) gets +the full API in headless mode, and `Session:request()` takes a plain callback — so the whole test +is `vim.wait()` polling a `done` flag per request, the same style nvim-dap's own test suite +(`spec/helpers.lua`) uses, rather than plenary or a coroutine wrapper. + +**`dap.listeners.on_session` closes the same race the VS Code test's `on_close` finding warns +about.** It fires the moment `dap.run()` creates the session object, before the session has even +connected — attaching `session.on_close[...]` there, rather than after some later step, is what +makes the close-detection reliable rather than occasionally racing the connection tearing down +first. Confirms, on the Neovim side, what the VS Code test found on its: `DapChannel` never sends +a `terminated`/`exited` DAP event, so both clients only learn a session is over from a dropped +socket (`Session:close()`'s `on_close` here, `onDidTerminateDebugSession` there). + +`nvim-dap` is fetched by `tests/run-tests.sh` into `tests/.deps/` (gitignored), pinned to a fixed +commit since the project carries no version tags — this test only exercises `bsh-dap.lua` +through it and vendors none of its code. Documented in +[`editors/neovim/README.md`](../editors/neovim/README.md#testing). + +### End-to-end GUI test for Eclipse + +Eclipse's generic DAP client has no headless, scriptable entry point equivalent to +`vscode.debug.startDebugging()` or `nvim-dap`'s `dap.run()` — an end-to-end test there would mean SWTBot or similar driving real windows, for less differentiated coverage, since [`editors/eclipse/`](../editors/eclipse/README.md) is already attach-only and so exercises less -of the extension-specific code the VS Code test caught its bug in. +of the extension-specific code the other two GUI tests caught real bugs in. ### GitHub Actions for builds — done diff --git a/editors/neovim/README.md b/editors/neovim/README.md index ce36b6e..cd38ef1 100644 --- a/editors/neovim/README.md +++ b/editors/neovim/README.md @@ -68,3 +68,27 @@ somewhere more visible. Same limits as the agent everywhere else, declared in its DAP capabilities rather than silently ignored: no pause (a BeanShell thread only stops where it calls the hook, so there is nothing to interrupt), no conditional/function/exception breakpoints, no step-back, no restart-frame. + +## Testing + +`tests/` drives `bsh-dap.lua` through a real, headless `nvim-dap` session against the fixture in +`tests/fixtures/script.bsh` — the same script, breakpoint line and evaluate expression +[`agent/checks/07-dap-transport.sh`](../../agent/checks/07-dap-transport.sh) and +[`../vscode/`](../vscode/README.md#testing)'s own test already prove work over `DapChannel`, +driven here through `dap.run()` instead of `dap-client.py` or `vscode.debug.startDebugging()`, so +it covers the one thing `07` cannot: this file's own `launch()` (the `jobstart` spawn and the +`DAP: listening` stdout watch). Assertions run against `session:request()` and +`dap.listeners.after.event_stopped`, since no UI is present to trigger `stackTrace`/`scopes`/ +`variables`/`evaluate` by clicking. Completion is detected via `Session:close()`'s `on_close` +hook rather than a `terminated` DAP event, since `DapChannel` never sends one — the JVM exiting +just drops the socket, the same finding the VS Code test made. + +```bash +./tests/run-tests.sh +``` + +Needs `nvim` (0.9+, for the `-l` script runner) and `git` on `PATH`. The first run clones +[`nvim-dap`](https://github.com/mfussenegger/nvim-dap) into `tests/.deps/`, pinned to a fixed +commit rather than a moving branch — this test does not vendor nvim-dap's code, only exercises +`bsh-dap.lua` through it. It also builds the agent jar itself, via the same +`:agent:samples:printPaths` Gradle task [`agent/checks/lib.sh`](../../agent/checks/lib.sh) uses. diff --git a/editors/neovim/tests/.gitignore b/editors/neovim/tests/.gitignore new file mode 100644 index 0000000..321b2a2 --- /dev/null +++ b/editors/neovim/tests/.gitignore @@ -0,0 +1 @@ +.deps/ diff --git a/editors/neovim/tests/fixtures/script.bsh b/editors/neovim/tests/fixtures/script.bsh new file mode 100644 index 0000000..1777a66 --- /dev/null +++ b/editors/neovim/tests/fixtures/script.bsh @@ -0,0 +1,10 @@ +// Same fixture as agent/checks/07-dap-transport.sh and editors/vscode's test (same breakpoint +// line and evaluate expression), proven here through a real nvim-dap session instead. +total = 0; +compute(n) { + doubled = n * 2; + return doubled + total; +} +total = 5; +print("result=" + compute(7)); +print("script done"); diff --git a/editors/neovim/tests/run-tests.sh b/editors/neovim/tests/run-tests.sh new file mode 100755 index 0000000..ffe7afe --- /dev/null +++ b/editors/neovim/tests/run-tests.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# +# End-to-end test of bsh-dap.lua: runs run.lua headless under a real nvim-dap, against the real +# agent. Needs `nvim` (0.9+, for the `-l` script runner) and `git` on PATH; this repo does not +# vendor nvim-dap, since it is nvim-dap's own -- not this repo's -- code under test. +# +# Usage: ./editors/neovim/tests/run-tests.sh + +set -uo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" +TESTS_DIR="$(pwd)" +REPO_ROOT="$(cd ../../.. && pwd)" +GRADLEW="$REPO_ROOT/gradlew" + +if ! command -v nvim >/dev/null; then + echo "nvim not found on PATH" >&2 + exit 1 +fi + +# Pinned to a commit, not a moving branch: nvim-dap carries no version tags, and a plugin this +# test only wires through bsh-dap.lua should not start failing because an unrelated upstream +# commit changed something this test happens to touch. +NVIM_DAP_REV="9e848e09a697ee95302a3ef2dd43fd6eb709e570" +DEPS_DIR="$TESTS_DIR/.deps" +NVIM_DAP_DIR="$DEPS_DIR/nvim-dap" +if [[ ! -d "$NVIM_DAP_DIR" ]]; then + mkdir -p "$DEPS_DIR" + git clone -q https://github.com/mfussenegger/nvim-dap.git "$NVIM_DAP_DIR" +fi +if [[ "$(git -C "$NVIM_DAP_DIR" rev-parse HEAD)" != "$NVIM_DAP_REV" ]]; then + git -C "$NVIM_DAP_DIR" checkout -q "$NVIM_DAP_REV" +fi + +echo "Resolving the agent jar and BeanShell classpath..." +paths="$("$GRADLEW" -q -p "$REPO_ROOT" :agent:samples:printPaths)" +BSH_CLASSPATH="$(grep '^BSH_CLASSPATH=' <<<"$paths" | cut -d= -f2-)" +AGENT_JAR="$(grep '^AGENT_JAR=' <<<"$paths" | cut -d= -f2-)" +if [[ -z "$BSH_CLASSPATH" || -z "$AGENT_JAR" ]]; then + echo "could not parse :agent:samples:printPaths output:" >&2 + echo "$paths" >&2 + exit 1 +fi + +REPO_ROOT="$REPO_ROOT" \ +NVIM_DAP_DIR="$NVIM_DAP_DIR" \ +BSH_AGENT_JAR="$AGENT_JAR" \ +BSH_CLASSPATH="$BSH_CLASSPATH" \ +FIXTURE_SCRIPT="$TESTS_DIR/fixtures/script.bsh" \ + nvim --headless -u NONE -l "$TESTS_DIR/run.lua" diff --git a/editors/neovim/tests/run.lua b/editors/neovim/tests/run.lua new file mode 100644 index 0000000..9d626a3 --- /dev/null +++ b/editors/neovim/tests/run.lua @@ -0,0 +1,126 @@ +-- Drives bsh-dap.lua through a real nvim-dap session, headless, and asserts on the DAP traffic -- +-- the same script, breakpoint line and evaluate expression agent/checks/07-dap-transport.sh +-- already proves work over DapChannel, and editors/vscode's own test proves work through a real +-- VS Code session. This is the Neovim counterpart: it covers what dap-client.py cannot, namely +-- bsh-dap.lua's own launch() -- the jobstart spawn and the "DAP: listening" stdout watch -- the +-- same gap the VS Code test was written to close for BshDebugAdapterDescriptorFactory.launch(). +-- +-- Run via run-tests.sh, which resolves nvim-dap and the agent paths and sets the env vars below. + +local REPO_ROOT = assert(os.getenv('REPO_ROOT'), 'REPO_ROOT not set') +local NVIM_DAP_DIR = assert(os.getenv('NVIM_DAP_DIR'), 'NVIM_DAP_DIR not set') +local AGENT_JAR = assert(os.getenv('BSH_AGENT_JAR'), 'BSH_AGENT_JAR not set') +local CLASSPATH = assert(os.getenv('BSH_CLASSPATH'), 'BSH_CLASSPATH not set') +local FIXTURE_SCRIPT = assert(os.getenv('FIXTURE_SCRIPT'), 'FIXTURE_SCRIPT not set') + +local BREAKPOINT_LINE = 6 -- `return doubled + total;` + +vim.opt.rtp:prepend(NVIM_DAP_DIR) +-- bsh-dap.lua is meant to be dropped directly onto runtimepath's lua/ (see its own README), not +-- nested under a lua/ subdirectory of its own, so it needs package.path rather than rtp:prepend. +package.path = REPO_ROOT .. '/editors/neovim/?.lua;' .. package.path + +local dap = require('dap') +require('bsh-dap').setup() + +vim.cmd.edit(FIXTURE_SCRIPT) +local bufnr = vim.api.nvim_get_current_buf() +require('dap.breakpoints').set({}, bufnr, BREAKPOINT_LINE) + +local stopped_events = {} +dap.listeners.after.event_stopped['e2e'] = function(_, body) + table.insert(stopped_events, body) +end + +-- DapChannel never sends a terminated/exited DAP event (the JVM just exits and the socket +-- drops), so nvim-dap only learns the session is over the way it always does on an unexpected +-- disconnect: Session:close() firing on_close, the same hook a real client relies on -- not a +-- message on the wire. on_session fires as soon as dap.run() creates the session, before it has +-- even connected, so this is race-free against the session closing before the hook is attached. +local closed = false +dap.listeners.on_session['e2e'] = function(_, new_session) + if new_session then + new_session.on_close['e2e'] = function() + closed = true + end + end +end + +-- session:request() would resume a coroutine automatically if called from inside one, but this +-- script runs on the main coroutine, so it gets an explicit callback and vim.wait() polls for +-- it -- the same style nvim-dap's own test suite (spec/helpers.lua) uses. +local function request(session, command, args) + local err, resp, done = nil, nil, false + session:request(command, args, function(e, r) + err, resp, done = e, r, true + end) + assert(vim.wait(10000, function() return done end, 20), command .. ' timed out') + assert(not err, command .. ' failed: ' .. vim.inspect(err)) + return resp +end + +dap.run({ + type = 'bsh', + request = 'launch', + name = 'e2e', + script = FIXTURE_SCRIPT, + agentJar = AGENT_JAR, + classpath = CLASSPATH, +}) + +-- Mirrors agent/checks/07-dap-transport.sh and the VS Code test: the first stop is the script's +-- own first statement, reported before the agent could know any breakpoints existed, not yet +-- inside compute() -- so this rides out stops until one actually lands there. +local thread_id, frame_names, stack +for attempt = 1, 4 do + assert( + vim.wait(10000, function() return #stopped_events >= attempt end, 50), + 'stop ' .. attempt .. ' never arrived' + ) + local body = stopped_events[attempt] + -- DapChannel deliberately sends the same generic "pause" for every stop -- it does not + -- distinguish "breakpoint" from "step" -- so that is what a real client sees here too. + assert(body.reason == 'pause', 'expected reason=pause, got ' .. tostring(body.reason)) + thread_id = body.threadId + + local session = assert(dap.session(), 'no session after a stopped event') + stack = request(session, 'stackTrace', { threadId = thread_id }) + frame_names = vim.tbl_map(function(f) return f.name end, stack.stackFrames) + if vim.tbl_contains(frame_names, 'compute') then + break + end + assert(attempt < 4, 'never reached a stop inside compute()') + request(session, 'continue', { threadId = thread_id }) +end +assert(#frame_names >= 2, 'expected the caller frame in the stack too') + +local session = assert(dap.session()) +local top_frame_id = stack.stackFrames[1].id +local scopes = request(session, 'scopes', { frameId = top_frame_id }) +local scope_names = vim.tbl_map(function(s) return s.name end, scopes.scopes) +assert(vim.tbl_contains(scope_names, 'Locals'), 'expected a Locals scope, got ' .. vim.inspect(scope_names)) +assert(vim.tbl_contains(scope_names, 'Global'), 'expected a Global scope, got ' .. vim.inspect(scope_names)) + +local locals_scope +for _, s in ipairs(scopes.scopes) do + if s.name == 'Locals' then + locals_scope = s + end +end +local variables = request(session, 'variables', { variablesReference = locals_scope.variablesReference }) +local doubled +for _, v in ipairs(variables.variables) do + if v.name == 'doubled' then + doubled = v + end +end +assert(doubled and doubled.value == '14', 'expected doubled=14, got ' .. vim.inspect(doubled)) + +local evaluated = request(session, 'evaluate', { expression = 'doubled + 1', frameId = top_frame_id }) +assert(evaluated.result == '15', 'expected evaluate result 15, got ' .. vim.inspect(evaluated.result)) + +request(session, 'continue', { threadId = thread_id }) +assert(vim.wait(10000, function() return closed end, 50), 'session never closed after script completion') + +print('bsh-dap.lua: all assertions passed') +os.exit(0) From 8fd9251a662fbaee23bfd7268cf51e3741626b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 09:46:25 +0200 Subject: [PATCH 09/12] Replace the Eclipse e2e test idea with a manual verification runbook Eclipse has no code of its own to regress -- editors/eclipse/ is a README, not a launcher, and LSP4E's generic Debug Adapter launch configuration is upstream code configured entirely through its own UI dialog. Automating it would mean standing up a second build toolchain (Tycho, a p2 target platform, SWTBot -- LSP4E ships via p2, not Maven Central) just to re-verify that LSP4E speaks DAP correctly against this agent, something 07-dap-transport.sh's dap-client.py already proves. Add a step-by-step manual checklist instead, against the same shared fixture (now duplicated at editors/eclipse/samples/script.bsh) and the same breakpoint line and evaluate expression as 07 and the VS Code/Neovim tests, including the one thing worth watching that only shows up against a real LSP4E session: what it does when DapChannel drops the socket instead of sending a terminated event. --- docs/FUTURE_WORK.md | 27 +++++++++++++++----- editors/eclipse/README.md | 41 ++++++++++++++++++++++++++++++ editors/eclipse/samples/script.bsh | 11 ++++++++ 3 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 editors/eclipse/samples/script.bsh diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index 4dbb8a0..d1975e1 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -191,13 +191,26 @@ commit since the project carries no version tags — this test only exercises `b through it and vendors none of its code. Documented in [`editors/neovim/README.md`](../editors/neovim/README.md#testing). -### End-to-end GUI test for Eclipse - -Eclipse's generic DAP client has no headless, scriptable entry point equivalent to -`vscode.debug.startDebugging()` or `nvim-dap`'s `dap.run()` — an end-to-end test there would mean -SWTBot or similar driving real windows, for less differentiated coverage, since -[`editors/eclipse/`](../editors/eclipse/README.md) is already attach-only and so exercises less -of the extension-specific code the other two GUI tests caught real bugs in. +### End-to-end GUI test for Eclipse — decided: a manual runbook instead + +Unlike VS Code and Neovim, [`editors/eclipse/`](../editors/eclipse/README.md) has no code of its +own to regress — it is a README, not a launcher; LSP4E's generic Debug Adapter launch +configuration is upstream code, configured entirely through its own UI dialog, and it is also +attach-only, so there is no launch step of ours to get wrong either. Automating it would mean +standing up a second build toolchain (Tycho, a p2 target platform, SWTBot — LSP4E ships via p2, +not Maven Central, so it can't join this Gradle build the lightweight way) just to re-verify that +*LSP4E* speaks DAP correctly against this agent, which +[`agent/checks/07-dap-transport.sh`](../agent/checks/07-dap-transport.sh)'s `dap-client.py` +already proves. Not worth a second build toolchain for coverage of code this repository doesn't +own. + +What replaced it: [`editors/eclipse/README.md#manual-verification-runbook`](../editors/eclipse/README.md#manual-verification-runbook) +is a step-by-step checklist against the same shared fixture (now at +[`editors/eclipse/samples/script.bsh`](../editors/eclipse/samples/script.bsh)) — same breakpoint +line, same evaluate expression as `07` and the other two editors' tests — to run by hand after +touching the agent or the DAP transport, including the one thing worth watching that only shows +up against a real LSP4E session: what it does when `DapChannel` drops the socket instead of +sending a `terminated` event. ### GitHub Actions for builds — done diff --git a/editors/eclipse/README.md b/editors/eclipse/README.md index a6ce8bb..53b5004 100644 --- a/editors/eclipse/README.md +++ b/editors/eclipse/README.md @@ -52,3 +52,44 @@ for the exact fields the launcher dialog exposes in your version. Same limits as the agent everywhere else, declared in its DAP capabilities rather than silently ignored: no pause (a BeanShell thread only stops where it calls the hook, so there is nothing to interrupt), no conditional/function/exception breakpoints, no step-back, no restart-frame. + +## Manual verification runbook + +Unlike [`../vscode/`](../vscode/#testing) and [`../neovim/`](../neovim/#testing), there is no +automated end-to-end test here. Both of those cover code this repository owns — the extension's +own JVM launch, `bsh-dap.lua`'s own launch — that a hand-rolled DAP client can't exercise. There +is no equivalent here: this package is a README, not a launcher, and LSP4E's generic Debug +Adapter launch configuration (configured entirely through its own UI dialog) is upstream code +this repository doesn't own. Automating it would mean standing up a second build toolchain +(Tycho, a p2 target platform, SWTBot) to re-verify that *LSP4E* speaks DAP correctly against this +agent — already proven, against the same agent, by +[`agent/checks/07-dap-transport.sh`](../../agent/checks/07-dap-transport.sh)'s `dap-client.py`. + +What's worth checking by hand — after touching the agent, the DAP transport, or this doc — is +that LSP4E's *own* attach flow still holds up end to end, using +[`samples/script.bsh`](samples/script.bsh) (the same fixture, breakpoint line and evaluate +expression `07` and the other two editors' tests already prove work): + +1. Resolve the agent jar and classpath: `./gradlew -q :agent:samples:printPaths` from the + repository root, giving `AGENT_JAR` and `BSH_CLASSPATH`. +2. Start the target JVM (see [step 1](#1-start-the-target-jvm) above), pointed at + `samples/script.bsh`, and confirm it blocks on `DAP: listening on 127.0.0.1:4711, waiting for + a client to attach` before doing anything else. +3. In Eclipse, open `samples/script.bsh` and set a line breakpoint on + `return doubled + total;` (line 6). +4. Create or reuse the **Debug Adapter** launch configuration (see + [step 2](#2-create-the-launch-configuration) above) and launch it. +5. Confirm, in that order: + - execution stops inside `compute()`, with the caller frame (`compute(7)` in `global`) also + visible in the call stack — not just the innermost frame; + - the Variables view offers both a **Locals** scope (`n = 7`, then `doubled = 14` once past + the assignment) and a **Global** scope (`total = 5`); + - evaluating `doubled + 1` (Display/Expressions view) returns `15`; + - resuming lets the script run to completion (`script done` on the target JVM's stdout) — + and, since `DapChannel` never sends a `terminated`/`exited` DAP event, watch what LSP4E + itself does when the socket merely drops: whether the Debug view marks the session + terminated on its own or is left stuck, since that is LSP4E's behaviour to characterize, not + this agent's to fix. + +A step that stops holding is a regression in the DAP transport itself — cross-check against `07` +and the VS Code/Neovim tests before assuming it's LSP4E's own behaviour that changed. diff --git a/editors/eclipse/samples/script.bsh b/editors/eclipse/samples/script.bsh new file mode 100644 index 0000000..7c54c7c --- /dev/null +++ b/editors/eclipse/samples/script.bsh @@ -0,0 +1,11 @@ +// Same fixture as agent/checks/07-dap-transport.sh, editors/vscode's test and editors/neovim's +// test (same breakpoint line and evaluate expression) -- walked through by hand here instead, per +// the manual verification runbook in ../README.md#manual-verification-runbook. +total = 0; +compute(n) { + doubled = n * 2; + return doubled + total; +} +total = 5; +print("result=" + compute(7)); +print("script done"); From bfa9b94f01535702636fb48e22881c0f257164e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 11:45:33 +0200 Subject: [PATCH 10/12] Rewrite the root README around what this repo actually is The old title ("bsh-plugin") and opening paragraph buried the two things this repository provides -- BeanShell language support for IntelliJ, and a debugger that also works standalone from VS Code, Neovim and Eclipse -- under a Gradle subproject listing. Lead with those up front, as their own headings for larger, unmissable heading text rather than a paragraph or plain bullets, and expand the plugin and editor sections so each stands on its own instead of pointing elsewhere immediately. Along the way: em dashes to match the rest of the document's typography, trailing whitespace, a duplicated blank line, and two accuracy fixes -- Eclipse's DAP support is LSP4E's generic client, not something built into Eclipse, and the VS Code extension only debugs, it doesn't separately "run" a script. "IntelliJ XDebug protocol" is reworded to "native protocol" to match the terminology the rest of the document already uses and avoid confusion with the unrelated PHP Xdebug. Also documents, in both CLAUDE.md files, that commits not yet pushed to a branch's upstream are fair game to restructure -- freely split, merge or reorder for the clearest history, distinct from the general Claude Code default of always making a new commit instead of amending a single one. --- CLAUDE.md | 3 ++ README.md | 132 ++++++++++++++++++++++++++++++++++++++--------- plugin/CLAUDE.md | 3 ++ 3 files changed, 114 insertions(+), 24 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4536596..358bd89 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -79,3 +79,6 @@ is the in-plugin hook class used by the *source-rewriting* fallback `fix:` tag. - **No `Co-Authored-By` trailer.** The maintainer reviews and edits every commit before pushing. +- **Commits not yet pushed to a branch's upstream are fair game to restructure.** Split, + merge, or reorder them freely — whatever makes the resulting history clearest thematically + and easiest to read. Once something is pushed, don't rewrite it without being asked. diff --git a/README.md b/README.md index 8bbde76..2652cf8 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,59 @@ -# bsh-plugin +# BeanShell Tooling — Debugging & Language Support for IDEs -BeanShell (`.bsh`) tooling: an IntelliJ Platform plugin for language support and an -in-editor debugger, plus a JVM debug agent that speaks the [Debug Adapter -Protocol][dap] so VS Code, Neovim and Eclipse can debug BeanShell too. +## IntelliJ-based IDEs plugin -One Gradle build, four subprojects: +JetBrains IDEA, WebStorm, CLion, and any other IntelliJ Platform IDE: + +- BeanShell script recognition — `.bsh` files, a self-executing shebang, or an + `` hint comment +- **Full language support** — a hand-written parser, syntax highlighting, code completion, + navigation, refactoring, running scripts +- **Debugging** — breakpoints, stepping, a variables view, evaluate +- **Maven `pom.xml` injection** — the same language support and debugging for BeanShell embedded + in a Maven plugin's `` + +## VS Code extension + +Debugging a `.bsh` script over DAP: attach to a JVM already running under the agent, or let the +extension launch it for you. + +## Neovim plugin + +Debugging support for `nvim-dap`, over the same DAP transport. + +## Eclipse + +Debugging support via LSP4E's generic DAP client (attach only — nothing here can launch the +target JVM itself). + +## Description + +This repository is two things, built together because the second depends on the first: a +source-level debugger for BeanShell, built once as a JVM agent and exposed twice — a native +protocol for the IntelliJ plugin above, and the Debug Adapter Protocol (DAP) for VS Code, Neovim +and Eclipse. See [`agent/`](agent/README.md) for the debugger and [`plugin/`](plugin/README.md) +for the language plugin. + +## Specific documentation + +**If you only want the IntelliJ IDE plugin**, [`plugin/README.md`](plugin/README.md) is the +complete reference — features, requirements, installation, screenshots. + +**If you want to debug BeanShell from an editor other than IntelliJ**, jump straight to +[`editors/`](#editors-vs-code-neovim-eclipse) below. + +## Why a debug agent, not JDWP + +BeanShell scripts are not their own class files — they are interpreted by `bsh.Interpreter` +line by line, so the JVM's own debugger (JDWP) has nothing to attach *to* at the script +level; it can only see the interpreter's Java internals. The agent instead instruments +`bsh.Interpreter` itself (via ASM, at class-load time, `-javaagent`-style) so it can suspend +a script at a source line, report locals from the interpreter's own namespace, and evaluate +expressions with the real interpreter — without modifying the script on disk or the library +that embeds it. [`agent/README.md`](agent/README.md) has the full rationale and the +landmines that came with it. + +## One Gradle build, four subprojects ``` plugin/ :plugin IntelliJ plugin -- language support and the debugger UI @@ -16,20 +65,53 @@ editors/ -- VS Code extension, Neovim/Eclipse config docs/ repository-wide docs ``` -## The two pieces - -**The IntelliJ plugin** ([`plugin/`](plugin/README.md)) adds BeanShell language -support to any IntelliJ-based IDE — syntax highlighting, a full AST parser, code -completion, navigation, running `.bsh` scripts, and Maven `pom.xml` injection. - -**The debug agent** ([`agent/README.md`](agent/README.md)) is a JVM agent that -instruments `bsh.Interpreter` so BeanShell scripts can be debugged at the source -level, without modifying the script or the library that embeds it. The IntelliJ -plugin bundles it and talks to it over a native protocol; the same agent also -speaks DAP, so it works as a standalone debug adapter for editors that have their -own DAP client — see [`editors/vscode/`](editors/vscode/README.md), -[`editors/neovim/`](editors/neovim/README.md) and -[`editors/eclipse/`](editors/eclipse/README.md). +**The agent is a separate subproject on purpose**: once it speaks DAP it is a debug adapter +that VS Code, Neovim or Eclipse can attach to, and none of them will take it out of an +IntelliJ plugin ZIP. The IntelliJ plugin bundles the same agent jar and talks to it over a +native protocol by default (richer — per-thread suspension, multiple simultaneous stops — +than what IntelliJ's own DAP client would support); the two protocols are just two +serializations of the same instrumentation underneath. + +## The IntelliJ plugin + +[`plugin/`](plugin/README.md) adds BeanShell language support to any IntelliJ-based IDE +(IDEA, WebStorm, PyCharm, CLion, …): + +- **Editing** — syntax highlighting, an AST-backed structure view, code folding, brace + matching, formatting, live/postfix templates, quick documentation. +- **Code intelligence** — a full recursive-descent parser matching the BeanShell grammar, + Go to Declaration / Find Usages / Rename, code completion, parameter hints, inspections + with quick fixes. +- **Java interoperability** (with the Java plugin present) — Ctrl+Click into Java classes + and members via static type propagation across a chain, and navigation into BeanShell + classes declared in a script. +- **Running** `.bsh` files with a bundled interpreter, and **Maven `pom.xml` injection** so + BeanShell embedded in Maven plugin configuration (`beanshell-maven-plugin`, the enforcer's + `evaluateBeanshell`, `build-helper-maven-plugin`, …) gets the same tooling. +- **Debugging** — line breakpoints, the call stack, Step Over/Into/Out, Run to Cursor, a + variables view with Watches and Evaluate, all backed by the real interpreter in the + selected frame. A companion Java (JDWP) session picks up breakpoints in Java code the + script calls into, for free, wherever the platform already wraps the JVM (e.g. debugging + an inline Maven script). + +Full details, screenshots and known limitations: [`plugin/README.md`](plugin/README.md). + +## Editors: VS Code, Neovim, Eclipse + +The debug agent doubles as a standalone DAP debug adapter (`-Dbsh.debug.protocol=dap`), so +editors with their own DAP client can debug BeanShell without the IntelliJ plugin at all: + +- [`editors/vscode/`](editors/vscode/README.md) — a VS Code extension with a `launch.json` + contribution and a `.bsh` language id. Supports both `attach` (to a JVM already running + under the agent) and `launch` (the extension starts that JVM itself). +- [`editors/neovim/`](editors/neovim/README.md) — configuration for `nvim-dap`, the same + transport. +- [`editors/eclipse/`](editors/eclipse/README.md) — configuration for Eclipse's generic DAP + client, attach-only (Eclipse's client has no scriptable way to launch the debuggee itself). + +What DAP doesn't cover yet — conditional/function/exception breakpoints, step-back, +restart-frame — is listed honestly in the adapter's own capabilities rather than silently +ignored; see [`docs/PROTOCOL.md`](docs/PROTOCOL.md#9-relationship-to-dap). ## Building @@ -41,16 +123,18 @@ JAVA_HOME= ./gradlew :plugin:test ./gradlew :agent:instrument:shadowJar # the agent jar alone ``` -The plugin needs **JDK 17+** (Gradle refuses less) and compiles Kotlin to 21. The -agent targets **Java 8**, because it loads into whatever JVM hosts BeanShell. +The plugin needs **JDK 17+** (Gradle refuses less) and compiles Kotlin to 21. The agent +targets **Java 8**, because it loads into whatever JVM hosts BeanShell — a per-task +`options.release`, not a build-wide property. ```bash ./agent/checks/run-all.sh # the agent, end to end ``` -`agent/checks/` runs what a JVM test cannot arrange from inside itself: a real -`mvn` process, a JVM launched with `-javaagent`, and two processes talking over -the debug socket. See [`agent/checks/README.md`](agent/checks/README.md). +`agent/checks/` runs what a JVM test cannot arrange from inside itself: a real `mvn` +process (so a real plugin realm), a JVM launched with `-javaagent`, and two processes +talking over the debug socket. Run it after touching the agent or the wire protocol — see +[`agent/checks/README.md`](agent/checks/README.md) for what each check protects. ## Where to read first diff --git a/plugin/CLAUDE.md b/plugin/CLAUDE.md index 9d4afb4..237d61e 100644 --- a/plugin/CLAUDE.md +++ b/plugin/CLAUDE.md @@ -49,6 +49,9 @@ wrapper lives one directory up and every task is addressed by path. Run these fr `fix:` tag. - **No `Co-Authored-By` trailer.** The maintainer reviews and edits every commit before pushing. +- **Commits not yet pushed to a branch's upstream are fair game to restructure.** Split, + merge, or reorder them freely — whatever makes the resulting history clearest thematically + and easiest to read. Once something is pushed, don't rewrite it without being asked. ## Testing notes From 82db1cf5928d074e3c4e9798e86bf6c3e20d88f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 12:58:50 +0200 Subject: [PATCH 11/12] Warm and cache the local Maven repo before the offline agent checks 02-maven-plugin-realm.sh runs `mvn -o` against sample poms that need build-helper-maven-plugin and maven-enforcer-plugin. On a fresh CI runner ~/.m2/repository is empty, so offline resolution fails before BeanShell ever runs, and every assertion in that check fails as "not found". --- .github/workflows/ci.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4d904b..2074d94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,5 +37,21 @@ jobs: # ubuntu-latest already ships Maven and Python 3, which the checks need # (02-maven-plugin-realm.sh, dap-client.py); nothing extra to install. + + # 02-maven-plugin-realm.sh runs `mvn -o` (offline) against these two poms, so their + # plugins must already be in ~/.m2/repository before that check runs. + - name: Cache local Maven repository + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-m2-${{ hashFiles('plugin/samples/maven/build-helper/pom.xml', 'plugin/samples/maven/enforcer/pom.xml') }} + restore-keys: | + ${{ runner.os }}-m2- + + - name: Warm the local Maven repo for the offline agent checks + run: | + mvn -q -f plugin/samples/maven/build-helper/pom.xml validate + mvn -q -f plugin/samples/maven/enforcer/pom.xml validate + - name: Run agent/checks run: ./agent/checks/run-all.sh From 70dde1228f691604d57d588f74cde2163c56230d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Lopat=C3=A1=C5=99?= Date: Wed, 29 Jul 2026 12:59:03 +0200 Subject: [PATCH 12/12] Dump raw command output when an agent check assertion fails Assertions in 02-maven-plugin-realm.sh check a file already filtered by grep, so a failure only ever reports "not found" -- never the mvn output that explains why. Give assert_contains/assert_not_contains an optional 4th argument for the unfiltered file and dump its tail on failure. --- agent/checks/02-maven-plugin-realm.sh | 10 +++++----- agent/checks/lib.sh | 16 ++++++++++++++-- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/agent/checks/02-maven-plugin-realm.sh b/agent/checks/02-maven-plugin-realm.sh index 406c72c..887984f 100755 --- a/agent/checks/02-maven-plugin-realm.sh +++ b/agent/checks/02-maven-plugin-realm.sh @@ -35,11 +35,11 @@ MAVEN_OPTS="-javaagent:$AGENT_JAR -Dbsh.debug.trace=1" \ grep 'bsh-agent' "$CHECK_TMP/bh.txt" > "$CHECK_TMP/bh-agent.txt" || true assert_contains "$CHECK_TMP/bh-agent.txt" 'src=inline evaluation of: ``prefix = project.getArtifactId();' \ - "build-helper: the inline is instrumented inside the plugin realm" + "build-helper: the inline is instrumented inside the plugin realm" "$CHECK_TMP/bh.txt" assert_contains "$CHECK_TMP/bh-agent.txt" 'line=1 src=inline evaluation of' \ - "build-helper: lines are snippet-relative (first statement is line 1)" + "build-helper: lines are snippet-relative (first statement is line 1)" "$CHECK_TMP/bh.txt" assert_contains "$CHECK_TMP/bh-agent.txt" 'line=3 src=inline evaluation of' \ - "build-helper: the third statement reports line 3" + "build-helper: the third statement reports line 3" "$CHECK_TMP/bh.txt" # --- the source-prefix filter ----------------------------------------------------------------- # @@ -60,7 +60,7 @@ MAVEN_OPTS="-javaagent:$AGENT_JAR -Dbsh.debug.trace=1 -Dbsh.debug.sources.file=$ grep 'bsh-agent' "$CHECK_TMP/filtered.txt" > "$CHECK_TMP/filtered-agent.txt" || true assert_contains "$CHECK_TMP/filtered-agent.txt" 'src=inline evaluation of' \ - "filter: a prefix computed by the production rule still matches the script" + "filter: a prefix computed by the production rule still matches the script" "$CHECK_TMP/filtered.txt" assert_not_contains "$CHECK_TMP/filtered-agent.txt" 'print.bsh' \ "filter: BeanShell's own print.bsh is excluded" @@ -83,7 +83,7 @@ if [[ -f "$ENFORCER" ]]; then mvn -o -q -f "$ENFORCER" validate > "$CHECK_TMP/enf.txt" 2>&1 grep 'bsh-agent' "$CHECK_TMP/enf.txt" > "$CHECK_TMP/enf-agent.txt" || true assert_contains "$CHECK_TMP/enf-agent.txt" 'src=inline evaluation of' \ - "enforcer: the inline is instrumented too" + "enforcer: the inline is instrumented too" "$CHECK_TMP/enf.txt" else printf ' \033[33mNOTE\033[0m no enforcer sample at %s, skipping that half\n' "$ENFORCER" fi diff --git a/agent/checks/lib.sh b/agent/checks/lib.sh index 4c0e7b2..cfb484d 100644 --- a/agent/checks/lib.sh +++ b/agent/checks/lib.sh @@ -33,19 +33,31 @@ fail() { fi } -# assert_contains +# Prints the tail of a file indented to line up under a fail() message. +_tail_context() { + printf ' --- last %d lines of %s ---\n' "$2" "$1" + tail -n "$2" "$1" | sed 's/^/ /' +} + +# assert_contains [] +# +# The optional 4th argument is the *unfiltered* command output the haystack was extracted from +# (e.g. by grep). On failure it's dumped to the log: the haystack alone is often just the empty +# result of that extraction, which says nothing about why -- the raw output usually does. assert_contains() { if grep -qF -- "$2" "$1"; then pass "$3" else fail "$3" "not found: $2" + [[ -n "${4:-}" && -f "$4" ]] && _tail_context "$4" 20 fi } -# assert_not_contains +# assert_not_contains [] assert_not_contains() { if grep -qF -- "$2" "$1"; then fail "$3" "unexpectedly found: $2" + [[ -n "${4:-}" && -f "$4" ]] && _tail_context "$4" 20 else pass "$3" fi