diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2074d94 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,57 @@ +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. + + # 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 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/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 diff --git a/docs/FUTURE_WORK.md b/docs/FUTURE_WORK.md index 5cf705b..d1975e1 100644 --- a/docs/FUTURE_WORK.md +++ b/docs/FUTURE_WORK.md @@ -101,18 +101,121 @@ 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, -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 + +`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). + +### 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 — 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 + +`.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. 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"); 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) 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" 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 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, diff --git a/plugin/tools/run-debug-bsh.sh b/plugin/tools/run-debug-bsh.sh index 9dcbb9b..4dc35ac 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' @@ -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}"