From 6aa4ff6986d3f8c4180d2620f667abc2ba3c0fc9 Mon Sep 17 00:00:00 2001 From: Martin Bens Date: Sun, 11 Oct 2026 23:41:55 +0200 Subject: [PATCH 1/2] refactor(dev-tooling)!: remove the optional phpactor PHP LSP The LSP added little for agents next to PHPStan through the `php-tooling` MCP server, and it carried a Python proxy, a root uv project and a separate CI job. Removes `lsp-server-php`, `.lsp.json`, the `lsp_bootstrap.sh`, `lsp_null.sh` and `lsp_proxy.py` scripts, the `lsp-directives.sh` SessionStart hook with its prompts, `docs/lsp.md`, and their tests. The shared `config.sh` drops the `CONFIG_FILE_PREFIX`/`CONFIG_ENV_VAR_PREFIX` branch that only the LSP launcher set. The root `pyproject.toml`, `uv.lock` and the `python` CI job served only the LSP proxy and are removed with it. The `dev-tooling-setting-up` skill no longer configures the LSP. BREAKING CHANGE: dev-tooling no longer registers a PHP language server, and `.lsp-php-tooling.json` and `LSP_PHP_TOOLING_CONFIG` are ignored. Use PHPStan through the `php-tooling` MCP server instead. Co-Authored-By: Claude --- .../software-writer/writing-code.md | 7 +- .../software-writer/writing-tests.md | 8 +- .../hook-contexts/writing-commit-messages.md | 2 +- .github/workflows/ci.yml | 32 -- README.md | 7 +- plugin-tests/README.md | 7 +- plugin-tests/dev-tooling/codex_manifest.bats | 16 +- plugin-tests/dev-tooling/lsp_bootstrap.bats | 252 ----------- plugin-tests/dev-tooling/lsp_null.bats | 84 ---- .../dev-tooling/lsp_proxy/conftest.py | 106 ----- .../dev-tooling/lsp_proxy/test_error_paths.py | 53 --- .../dev-tooling/lsp_proxy/test_framing.py | 122 ----- .../dev-tooling/lsp_proxy/test_lifecycle.py | 124 ----- .../dev-tooling/lsp_proxy/test_uri_rewrite.py | 105 ----- plugin-tests/mcp-shared/config.bats | 42 +- .../dev-tooling/.claude-plugin/plugin.json | 6 +- plugins/dev-tooling/.codex-plugin/plugin.json | 2 +- plugins/dev-tooling/.lsp.json | 8 - plugins/dev-tooling/AGENTS.md | 40 +- plugins/dev-tooling/CHANGELOG.md | 7 + plugins/dev-tooling/README.md | 12 +- plugins/dev-tooling/SETUP.md | 168 ------- plugins/dev-tooling/docs/lsp.md | 62 --- plugins/dev-tooling/docs/mcp-enforcement.md | 4 +- plugins/dev-tooling/hooks/hooks.json | 7 +- .../hooks/prompts/lsp-directives-header.md | 21 - .../hooks/prompts/lsp-directives-php.md | 17 - .../hooks/scripts/lsp-directives.sh | 64 --- .../lsp-server-php/lib/phpactor.sh | 12 - plugins/dev-tooling/lsp-server-php/lsp.sh | 29 -- plugins/dev-tooling/shared/config.sh | 24 +- plugins/dev-tooling/shared/lsp_bootstrap.sh | 192 -------- plugins/dev-tooling/shared/lsp_null.sh | 99 ---- plugins/dev-tooling/shared/lsp_proxy.py | 277 ------------ .../plugin-setup/.claude-plugin/plugin.json | 2 +- plugins/plugin-setup/CHANGELOG.md | 5 + .../SKILL.md | 2 +- .../skills/dev-tooling-setting-up/SKILL.md | 4 +- .../references/plugin-setup.md | 168 ------- plugins/shopware-env/shared/config.sh | 24 +- pyproject.toml | 60 --- templates/README.md | 4 +- templates/mcp-shared/config.sh | 24 +- templates/plugin-setup/SKILL.md | 6 +- uv.lock | 428 ------------------ 45 files changed, 68 insertions(+), 2677 deletions(-) delete mode 100644 plugin-tests/dev-tooling/lsp_bootstrap.bats delete mode 100644 plugin-tests/dev-tooling/lsp_null.bats delete mode 100644 plugin-tests/dev-tooling/lsp_proxy/conftest.py delete mode 100644 plugin-tests/dev-tooling/lsp_proxy/test_error_paths.py delete mode 100644 plugin-tests/dev-tooling/lsp_proxy/test_framing.py delete mode 100644 plugin-tests/dev-tooling/lsp_proxy/test_lifecycle.py delete mode 100644 plugin-tests/dev-tooling/lsp_proxy/test_uri_rewrite.py delete mode 100644 plugins/dev-tooling/.lsp.json delete mode 100644 plugins/dev-tooling/docs/lsp.md delete mode 100644 plugins/dev-tooling/hooks/prompts/lsp-directives-header.md delete mode 100644 plugins/dev-tooling/hooks/prompts/lsp-directives-php.md delete mode 100755 plugins/dev-tooling/hooks/scripts/lsp-directives.sh delete mode 100644 plugins/dev-tooling/lsp-server-php/lib/phpactor.sh delete mode 100755 plugins/dev-tooling/lsp-server-php/lsp.sh delete mode 100644 plugins/dev-tooling/shared/lsp_bootstrap.sh delete mode 100755 plugins/dev-tooling/shared/lsp_null.sh delete mode 100644 plugins/dev-tooling/shared/lsp_proxy.py delete mode 100644 pyproject.toml delete mode 100644 uv.lock diff --git a/.claude/extensions/software-writer/writing-code.md b/.claude/extensions/software-writer/writing-code.md index 1681f67a..fe79e054 100644 --- a/.claude/extensions/software-writer/writing-code.md +++ b/.claude/extensions/software-writer/writing-code.md @@ -3,9 +3,8 @@ - `project.stacks` = | Stack | Where | Toolchain | |---|---|---| - | bash | `plugins/*/hooks/scripts/`, `plugins/*/mcp-server-*/`, `plugins/*/lsp-server-*/`, `plugins/*/shared/`, `templates/`, `.github/scripts/`, `plugin-tests/**/*.bats` | bash 4.0+ for MCP servers, 4.4+ for `test-writing` (`local -`); bash 3.2+ for `.github/scripts/`; `jq`; ShellCheck v0.11.0 in CI | - | python (root project) | `plugins/dev-tooling/shared/lsp_proxy.py`, `plugin-tests/dev-tooling/lsp_proxy/` | 3.12, `uv`; ruff line-length 100 selecting `E W F I B UP SIM RUF`; mypy `strict`, scoped by `files` to exactly these two paths and nothing else | - | python (`agent-skills-export/`) | `agent-skills-export/src/`, `agent-skills-export/tests/` | A separate `uv` project with its own lockfile, its own ruff (adds `C4 ARG PTH`, **ignores `E501`**), and its own mypy `strict`. Root ruff `extend-exclude`s it and root mypy does not see it — never apply the root project's rules here. | + | bash | `plugins/*/hooks/scripts/`, `plugins/*/mcp-server-*/`, `plugins/*/shared/`, `templates/`, `.github/scripts/`, `plugin-tests/**/*.bats` | bash 4.0+ for MCP servers, 4.4+ for `test-writing` (`local -`); bash 3.2+ for `.github/scripts/`; `jq`; ShellCheck v0.11.0 in CI | + | python (`agent-skills-export/`) | `agent-skills-export/src/`, `agent-skills-export/tests/` | A separate `uv` project with its own lockfile, its own ruff (adds `C4 ARG PTH`, **ignores `E501`**), and its own mypy `strict`. | No other stack is in play. PHP, TypeScript, and Twig appear only as the *subjects* the plugins operate on in other repositories, never as source here. - `code.primitives` = @@ -26,7 +25,7 @@ | Resolve which scope a tool call runs under | reading `.scopes` out of the config at each call site | `resolve_scope`, `scope_get_tool_field`, `scope_get_bootstrap` (`plugins/dev-tooling/shared/scope.sh`, dev-tooling-owned and deliberately not templated) | Validates once — rejects the reserved name `shopware`, rejects an undeclared `default_scope` — and sets `SCOPE_NAME` and `SCOPE_CWD` for every `tool_*` function, so no tool re-derives a scope and none disagree about it. | | Read a hook's stdin, load its config, or block a command | `cat` plus `jq`, then `echo >&2; exit 2` | `parse_hook_input`, `load_mcp_config`, `block_tool` (`templates/hooks-shared/common.sh`) | Carries the hook input contract, the `enforce_mcp_tools` opt-out, and the exit-2 block protocol with its message shape. | - `code.di_pattern` = The outermost injection is `plugins//.mcp.json`, which points each server name at `${CLAUDE_PLUGIN_ROOT}/mcp-server-*/server.sh`; `${CLAUDE_PLUGIN_ROOT}` is the one value supplied from outside the repository. Below it, a bash MCP server's composition root is `plugins//mcp-server-*/server.sh`: it sets and exports the module inputs (`MCP_CONFIG_FILE`, `MCP_TOOLS_LIST_FILE`, `MCP_LOG_FILE`, `PROJECT_ROOT`, `CONFIG_PREFIX`), then sources the shared modules in dependency order and calls `run_mcp_server`. A sourced module reads its inputs from those exported variables and takes everything else as positional parameters; it never discovers its own configuration. Hook scripts take their input as JSON on stdin via `parse_hook_input`. Python uses parameter injection (`Path` arguments), never module-level state. -- `code.export_conventions` = A leading `_` marks a sourced module's internal function or variable (`_merge_configs`, `_discover_configs`, `_FOUND_CONFIGS`); unprefixed names are that file's public API. A sourced module that has a non-obvious public surface states it in a `# Public:` header block listing each exported function with a one-line contract, as `plugins/dev-tooling/shared/scope.sh` does — write one when adding a module other files will source, and keep it current when the surface changes. Python mirrors the underscore rule: `__init__.py` declares `__all__` and re-exports the public surface from `core.py`; `_`-prefixed names stay private, including in the standalone `lsp_proxy.py`, which has no package boundary and so no `__all__`. +- `code.export_conventions` = A leading `_` marks a sourced module's internal function or variable (`_merge_configs`, `_discover_configs`, `_FOUND_CONFIGS`); unprefixed names are that file's public API. A sourced module that has a non-obvious public surface states it in a `# Public:` header block listing each exported function with a one-line contract, as `plugins/dev-tooling/shared/scope.sh` does — write one when adding a module other files will source, and keep it current when the surface changes. Python mirrors the underscore rule: `__init__.py` declares `__all__` and re-exports the public surface from `core.py`; `_`-prefixed names stay private. An MCP tool is exported by doing **both**: defining `tool_` in a sourced lib, and adding its entry with an `inputSchema` to the server's `tools.json`. `handle_tools_call` dispatches via `type "tool_${tool_name}"` and `validate_tool_arguments` validates the call against that entry. A function without its `tools.json` entry is unreachable; an entry without an `inputSchema` is dispatched unvalidated. - `code.footgun_additions` = diff --git a/.claude/extensions/software-writer/writing-tests.md b/.claude/extensions/software-writer/writing-tests.md index 3db83b63..743eac35 100644 --- a/.claude/extensions/software-writer/writing-tests.md +++ b/.claude/extensions/software-writer/writing-tests.md @@ -3,16 +3,14 @@ - `project.stacks` = | Stack | Where | Toolchain | |---|---|---| - | bash | `plugins/*/hooks/scripts/`, `plugins/*/mcp-server-*/`, `plugins/*/lsp-server-*/`, `plugins/*/shared/`, `templates/`, `.github/scripts/`, `plugin-tests/**/*.bats` | bash 4.0+ for MCP servers, 4.4+ for `test-writing` (`local -`); bash 3.2+ for `.github/scripts/`; `jq`; ShellCheck v0.11.0 in CI | - | python (root project) | `plugins/dev-tooling/shared/lsp_proxy.py`, `plugin-tests/dev-tooling/lsp_proxy/` | 3.12, `uv`; ruff line-length 100 selecting `E W F I B UP SIM RUF`; mypy `strict`, scoped by `files` to exactly these two paths and nothing else | - | python (`agent-skills-export/`) | `agent-skills-export/src/`, `agent-skills-export/tests/` | A separate `uv` project with its own lockfile, its own ruff (adds `C4 ARG PTH`, **ignores `E501`**), and its own mypy `strict`. Root ruff `extend-exclude`s it and root mypy does not see it — never apply the root project's rules here. | + | bash | `plugins/*/hooks/scripts/`, `plugins/*/mcp-server-*/`, `plugins/*/shared/`, `templates/`, `.github/scripts/`, `plugin-tests/**/*.bats` | bash 4.0+ for MCP servers, 4.4+ for `test-writing` (`local -`); bash 3.2+ for `.github/scripts/`; `jq`; ShellCheck v0.11.0 in CI | + | python (`agent-skills-export/`) | `agent-skills-export/src/`, `agent-skills-export/tests/` | A separate `uv` project with its own lockfile, its own ruff (adds `C4 ARG PTH`, **ignores `E501`**), and its own mypy `strict`. | No other stack is in play. PHP, TypeScript, and Twig appear only as the *subjects* the plugins operate on in other repositories, never as source here. - `tests.frameworks` = | Stack | Framework | Test files | Run | |---|---|---|---| | bash | BATS — bats-core 1.13.0, bats-support, bats-assert 2.2.4; every file declares `bats_require_minimum_version 1.11.0` | `plugin-tests//*.bats` | `./.github/scripts/setup-bats.sh` once, then `.bats/bats-core/bin/bats plugin-tests/**/*.bats`; CI runs `bats --timing -r plugin-tests/` | - | python | pytest with `asyncio_mode = "auto"` and `pytest-subprocess` | `plugin-tests/dev-tooling/lsp_proxy/test_*.py` | `uv run pytest plugin-tests/dev-tooling/lsp_proxy/` | | python | pytest, a separate uv project with its own dependency set | `agent-skills-export/tests/test_*.py` | `make test` (`uv run --project agent-skills-export pytest agent-skills-export/tests/`) | Select per file being edited. A `.bats` file is bash regardless of what language the script under test drives. @@ -20,7 +18,7 @@ - `plugin-tests/test_helper/common_setup.bash` — `run_hook`, `assert_hook_blocks`, and `REPO_ROOT` resolution; loads bats-support and bats-assert. Every `.bats` file reaches it through its plugin helper. - `plugin-tests//test_helper/common_setup.bash` — sets `SCRIPTS_DIR` for that plugin and holds its own builders, such as dev-tooling's `setup_config` and `setup_php_mcp_env`. - `plugin-tests/dev-tooling/fixtures/coverage/*.xml` — committed Clover reports, read with `$(< file)` in `setup()`. - - `plugin-tests/dev-tooling/lsp_proxy/conftest.py` and `agent-skills-export/tests/conftest.py` — the pytest fixtures for their respective suites. + - `agent-skills-export/tests/conftest.py` — the pytest fixtures for that suite. ## Pre-Step-1 diff --git a/.claude/hook-contexts/writing-commit-messages.md b/.claude/hook-contexts/writing-commit-messages.md index 2091fde4..9eeb9871 100644 --- a/.claude/hook-contexts/writing-commit-messages.md +++ b/.claude/hook-contexts/writing-commit-messages.md @@ -50,7 +50,7 @@ Infrastructure scopes: Scope omission: - Root docs only (`README.md`, `CONTRIBUTING.md`, `AGENTS.md`, `LICENSE`): omit scope. -- Root configs only (`pyproject.toml`, `uv.lock`, `.gitignore`): omit scope. +- Root configs only (`.gitignore`, `.shellcheckrc`): omit scope. - Cross-cutting changes spanning 3 or more unrelated plugins: omit scope. - Type is `ci` with only generic CI changes (not plugin-specific workflows): omit scope. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 58cf69c3..ea02f665 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,6 @@ on: - '.agents/**' - '.claude-plugin/**' - '.mcp-sdk.lock' - - 'pyproject.toml' - - 'uv.lock' - '.shellcheckrc' - '.github/workflows/ci.yml' - '.github/scripts/**' @@ -21,8 +19,6 @@ on: - '.agents/**' - '.claude-plugin/**' - '.mcp-sdk.lock' - - 'pyproject.toml' - - 'uv.lock' - '.shellcheckrc' - '.github/workflows/ci.yml' - '.github/scripts/**' @@ -71,31 +67,3 @@ jobs: run: | echo "Running BATS tests with timing..." bats --timing -r plugin-tests/ - - python: - name: Python Tests & Lint - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - - name: Install uv - uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0 - with: - enable-cache: true - - - name: Install Python - run: uv python install - - - name: Sync dependencies - run: uv sync --locked --group test --group lint - - - name: Ruff - run: uv run ruff check . - - - name: Mypy - run: uv run mypy - - - name: Pytest - run: uv run pytest plugin-tests/dev-tooling/lsp_proxy/ -v diff --git a/README.md b/README.md index 08768c7b..22ff62a1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ These plugins work best alongside a few Claude Code tweaks. Turn on `ENABLE_TOOL | Plugin | Description | Components | |-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------| -| [dev-tooling](#dev-tooling) | PHPStan, ECS, PHPUnit, ESLint, Stylelint, Jest, and more via MCP servers. Optional phpactor LSP. | 🔌 MCP · 🪝 Hooks · 🎯 Skills · 🧠 LSP · 🤖 Agents | +| [dev-tooling](#dev-tooling) | PHPStan, ECS, PHPUnit, ESLint, Stylelint, Jest, and more via MCP servers. | 🔌 MCP · 🪝 Hooks · 🤖 Agents | | [test-writing](#test-writing) | Automated PHPUnit test generation and validation for Shopware 6. | 🎯 Skills · 🤖 Agents · 🔌 MCP | | [chunkhound-integration](#chunkhound-integration) | Semantic code research using ChunkHound. | 🔌 MCP · 🪝 Hooks · 🎯 Skills · 🤖 Agents | | [ci-failure-interpretation](#ci-failure-interpretation) | CI failure log interpretation for GitHub Actions workflows. | 🎯 Skills | @@ -48,7 +48,7 @@ These plugins work best alongside a few Claude Code tweaks. Turn on `ENABLE_TOOL ### dev-tooling -Three MCP servers for PHP and JavaScript operations plus an optional phpactor LSP for active PHP code discovery. Supports native, Docker, Docker Compose, Vagrant, and DDEV environments. Runs on Claude Code and on OpenAI Codex, where the servers start without a project root and bind one on demand; the LSP, the runner agent, and the worktree reminder hook are Claude Code only. +Three MCP servers for PHP and JavaScript operations. Supports native, Docker, Docker Compose, Vagrant, and DDEV environments. Runs on Claude Code and on OpenAI Codex, where the servers start without a project root and bind one on demand; the runner agent and the worktree reminder hook are Claude Code only. ```bash /plugin install dev-tooling@shopware-ai-coding-tools @@ -60,9 +60,8 @@ codex plugin add dev-tooling@shopware-ai-coding-tools - **PHP:** PHPStan static analysis, ECS code style, PHPUnit test runner with coverage gap analysis, Symfony Console, Rector - **Administration JS:** ESLint, Stylelint, Prettier, Jest, TypeScript, Vite builds - **Storefront JS:** ESLint, Stylelint, Jest, Webpack builds -- **PHP LSP (optional):** document symbols, hover, go-to-definition, and references via [phpactor](https://github.com/phpactor/phpactor) -After installing, also install `plugin-setup@shopware-ai-coding-tools`, then ask Claude to help you set up the plugin — the `dev-tooling-setting-up` skill walks you through configuration. Prerequisites: `bash` 4.1+ (macOS's stock `/bin/bash` is 3.2), `jq`, and a restart after install. For LSP: `phpactor` binary available on the host (native) or inside the container (docker/docker-compose/vagrant/ddev). On Codex, trust the plugin hooks when Codex asks: the SessionStart directive then names the session directory, and the model binds each server by calling `set_project_root` itself. A server binds only to a project that already holds its configuration file (`.mcp-php-tooling.json` for `php-tooling`, `.mcp-js-tooling.json` for the two JS servers) unless `MCP_PHP_TOOLING_CONFIG` or `MCP_JS_TOOLING_CONFIG` names an existing file, and `plugin-setup` has no Codex manifest, so write that file by hand. Exporting `PROJECT_ROOT` before starting Codex is the alternative; see the [plugin README](./plugins/dev-tooling/README.md) for its caveat. +After installing, also install `plugin-setup@shopware-ai-coding-tools`, then ask Claude to help you set up the plugin — the `dev-tooling-setting-up` skill walks you through configuration. Prerequisites: `bash` 4.1+ (macOS's stock `/bin/bash` is 3.2), `jq`, and a restart after install. On Codex, trust the plugin hooks when Codex asks: the SessionStart directive then names the session directory, and the model binds each server by calling `set_project_root` itself. A server binds only to a project that already holds its configuration file (`.mcp-php-tooling.json` for `php-tooling`, `.mcp-js-tooling.json` for the two JS servers) unless `MCP_PHP_TOOLING_CONFIG` or `MCP_JS_TOOLING_CONFIG` names an existing file, and `plugin-setup` has no Codex manifest, so write that file by hand. Exporting `PROJECT_ROOT` before starting Codex is the alternative; see the [plugin README](./plugins/dev-tooling/README.md) for its caveat. See [full documentation](./plugins/dev-tooling/README.md) for configuration and tool reference. diff --git a/plugin-tests/README.md b/plugin-tests/README.md index 8cbd5cd4..d708a027 100644 --- a/plugin-tests/README.md +++ b/plugin-tests/README.md @@ -52,7 +52,7 @@ plugin-tests/ ├── test_helper/ │ └── common_setup.bash # Shared core fixtures ├── mcp-shared/ # Suites for templates/mcp-shared/ modules -│ ├── config.bats # Config filename and env-var prefix parameterization +│ ├── config.bats # Config filename and env-var derivation from CONFIG_PREFIX │ ├── docker_compose.bats # Docker Compose call-time container/workdir resolution │ ├── environment.bats # Environment wrapping │ └── scope_wrap.bats # Scope-aware command wrapping per environment @@ -60,8 +60,6 @@ plugin-tests/ │ ├── php_tools.bats # PHP hook blocking │ ├── js_admin_tools.bats # Admin JS hook blocking │ ├── js_storefront_tools.bats # Storefront JS hook blocking -│ ├── lsp_bootstrap.bats # LSP bootstrap: null stub, direct exec or python proxy -│ ├── lsp_null.bats # The minimal LSP stub used when an LSP is disabled │ ├── mcp_tool_console.bats # Console tool tests │ ├── mcp_tool_ecs.bats # ECS tool tests │ ├── mcp_tool_js_admin.bats # Admin JS MCP tool tests @@ -76,7 +74,6 @@ plugin-tests/ │ ├── scope_resolution.bats # Scope declaration, validation and refusal │ ├── scope_session_start.bats # Scope reporting from the session-start hook │ ├── session_start.bats # Session-start hook JSON output -│ ├── lsp_proxy/ # pytest suite for shared/lsp_proxy.py │ ├── fixtures/ │ │ └── coverage/ # XML fixtures for coverage gap tests │ │ ├── two_files.xml # Two files with partial coverage @@ -167,7 +164,7 @@ The MCP protocol handler (`plugins/*/shared/mcpserver_core.sh`) has no suite her ## 🔄 CI -Tests run via GitHub Actions (`.github/workflows/ci.yml`) on changes to `plugins/**`, `plugin-tests/**`, `pyproject.toml`, `uv.lock`, `.shellcheckrc`, `.github/workflows/ci.yml`, or `.github/scripts/**`. +Tests run via GitHub Actions (`.github/workflows/ci.yml`) on changes to `plugins/**`, `plugin-tests/**`, `.agents/**`, `.claude-plugin/**`, `.mcp-sdk.lock`, `.shellcheckrc`, `.github/workflows/ci.yml`, or `.github/scripts/**`. ## 📌 Dependencies diff --git a/plugin-tests/dev-tooling/codex_manifest.bats b/plugin-tests/dev-tooling/codex_manifest.bats index 59670c13..a28c009c 100644 --- a/plugin-tests/dev-tooling/codex_manifest.bats +++ b/plugin-tests/dev-tooling/codex_manifest.bats @@ -64,27 +64,13 @@ _assert_server_forwards_config_vars() { assert_output "$(jq -r '.version' "${CLAUDE_MANIFEST}")" } -# The description and the keywords are the fields the two hosts differ on: the -# LSP is Claude Code only, and the Claude manifest advertises it in both. # bats test_tags=static @test "the codex manifest copies the claude manifest's shared metadata" { - local fields='{name, author, license, homepage, repository}' + local fields='{name, description, author, license, keywords, homepage, repository}' run jq -cS "${fields}" "${CODEX_MANIFEST}" assert_output "$(jq -cS "${fields}" "${CLAUDE_MANIFEST}")" } -# bats test_tags=static -@test "the codex manifest keywords are the claude manifest keywords without the LSP ones" { - run jq -c '.keywords' "${CODEX_MANIFEST}" - assert_output "$(jq -c '.keywords - ["lsp", "language-server"]' "${CLAUDE_MANIFEST}")" -} - -# bats test_tags=static -@test "the codex manifest description does not advertise the LSP" { - run jq -r '.description | test("\\blsp\\b|phpactor|language server"; "i")' "${CODEX_MANIFEST}" - assert_output "false" -} - # Without a hooks field Codex loads hooks/hooks.json itself; a hooks field # would replace that file rather than add to it. # bats test_tags=static diff --git a/plugin-tests/dev-tooling/lsp_bootstrap.bats b/plugin-tests/dev-tooling/lsp_bootstrap.bats deleted file mode 100644 index 029248a3..00000000 --- a/plugin-tests/dev-tooling/lsp_bootstrap.bats +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env bats -# bats file_tags=dev-tooling,lsp,bootstrap -bats_require_minimum_version 1.11.0 - -# Tests for shared/lsp_bootstrap.sh. -# The bootstrap is sourced by lsp-server-/lsp.sh files. It reads -# .lsp--tooling.json and decides between: null stub, direct exec, or -# python proxy. -# -# These tests exercise the decision logic by: -# 1. Creating a temp project dir with a test config file -# 2. Sourcing the bootstrap in a subshell with PROJECT_ROOT pointing at the temp dir -# 3. Asserting that `lsp_run_or_null_stub` dispatches to the expected target - -setup() { - SHARED_DIR="$(cd "${BATS_TEST_DIRNAME}/../../plugins/dev-tooling/shared" && pwd)" - TMPDIR_PROJECT=$(mktemp -d) - export TMPDIR_PROJECT -} - -teardown() { - rm -rf "${TMPDIR_PROJECT}" -} - -_write_config() { - local json="$1" - mkdir -p "${TMPDIR_PROJECT}/.claude" - printf '%s' "$json" > "${TMPDIR_PROJECT}/.claude/.lsp-php-tooling.json" -} - -# Common preamble for the `bash -c` block of every test. Keeps the bootstrap -# contract (SCRIPT_DIR, SHARED_DIR, PROJECT_ROOT, CONFIG_*, LSP_DEFAULT_BINARY, -# silent log stub, dry-run flag) in one place so tests only contain what's -# unique to them. -_bootstrap_preamble() { - printf '%s\n' \ - "set -uo pipefail" \ - "SCRIPT_DIR='${SHARED_DIR}/../lsp-server-php'" \ - "SHARED_DIR='${SHARED_DIR}'" \ - "PROJECT_ROOT='${TMPDIR_PROJECT}'" \ - "CONFIG_PREFIX='php-tooling'" \ - "CONFIG_FILE_PREFIX='.lsp-'" \ - "CONFIG_ENV_VAR_PREFIX='LSP'" \ - "LSP_DEFAULT_BINARY='phpactor'" \ - "log() { :; }; export -f log" \ - "export LSP_DISPATCH_DRY_RUN=1" -} - -@test "missing config file -> LSP_DISPATCH_TARGET=null-stub" { - # No config written. - run bash -c " -$(_bootstrap_preamble) -source '${SHARED_DIR}/lsp_bootstrap.sh' -lsp_run_or_null_stub 'phpactor language-server' -" - [ "$status" -eq 0 ] - [[ "$output" == *"target=null-stub"* ]] -} - -@test "enabled=false -> null-stub" { - _write_config '{"environment":"native","enabled":false,"binary":"phpactor"}' - run bash -c " -$(_bootstrap_preamble) -source '${SHARED_DIR}/lsp_bootstrap.sh' -lsp_run_or_null_stub 'phpactor language-server' -" - [ "$status" -eq 0 ] - [[ "$output" == *"target=null-stub"* ]] -} - -@test "enabled=true + environment=native -> direct-exec with binary" { - _write_config '{"environment":"native","enabled":true,"binary":"phpactor"}' - run bash -c " -$(_bootstrap_preamble) -source '${SHARED_DIR}/lsp_bootstrap.sh' -lsp_run_or_null_stub 'phpactor language-server' -" - [ "$status" -eq 0 ] - [[ "$output" == *"target=direct-exec"* ]] - [[ "$output" == *"cmd=phpactor language-server"* ]] -} - -@test "enabled=true + environment missing -> hard fail with message" { - _write_config '{"enabled":true,"binary":"phpactor"}' - # Override the silent log stub so detect_environment's error reaches $output. - run bash -c " -$(_bootstrap_preamble) -log() { printf '[%s] %s\n' \"\$1\" \"\${*:2}\"; }; export -f log -source '${SHARED_DIR}/lsp_bootstrap.sh' -lsp_run_or_null_stub 'phpactor language-server' -" - [ "$status" -ne 0 ] - [[ "$output" == *"Missing 'environment' field"* ]] -} - -@test "binary override is read from config" { - _write_config '{"environment":"native","enabled":true,"binary":"/opt/phpactor/bin/phpactor"}' - run bash -c " -$(_bootstrap_preamble) -source '${SHARED_DIR}/lsp_bootstrap.sh' -echo \"LSP_BINARY=\${LSP_BINARY}\" -" - [ "$status" -eq 0 ] - [[ "$output" == *"LSP_BINARY=/opt/phpactor/bin/phpactor"* ]] -} - -@test "containerized + preflight passes -> python-proxy" { - # Write a docker-compose config. The bootstrap will call - # _lsp_preflight_container_binary to verify the binary inside the container. - # We override that function to return 0, simulating a passing preflight - # without needing docker to be installed on the test host. We also override - # _compose_resolve_workdir so the test doesn't depend on the real function - # finding COMPOSE_WORKDIR_OVERRIDE in environment.sh's config parsing. - _write_config '{"environment":"docker-compose","docker-compose":{"service":"web","workdir":"/var/www/html"},"enabled":true,"binary":"phpactor"}' - run bash -c " -$(_bootstrap_preamble) -source '${SHARED_DIR}/lsp_bootstrap.sh' -_lsp_preflight_container_binary() { return 0; } -_compose_resolve_workdir() { echo '/var/www/html'; } -wrap_command() { echo \"docker compose exec -T web \$1\"; } -lsp_run_or_null_stub 'phpactor language-server' -" - [ "$status" -eq 0 ] - [[ "$output" == *"target=python-proxy"* ]] - [[ "$output" == *"wrapped=docker compose exec -T web phpactor language-server"* ]] -} - -@test "containerized + preflight fails -> null-stub" { - _write_config '{"environment":"docker-compose","docker-compose":{"service":"web","workdir":"/var/www/html"},"enabled":true,"binary":"phpactor"}' - run bash -c " -$(_bootstrap_preamble) -source '${SHARED_DIR}/lsp_bootstrap.sh' -_lsp_preflight_container_binary() { return 1; } # force fail -wrap_command() { echo \"docker compose exec -T web \$1\"; } -lsp_run_or_null_stub 'phpactor language-server' -" - [ "$status" -eq 0 ] - [[ "$output" == *"target=null-stub"* ]] - [[ "$output" == *"preflight failed"* ]] -} - -@test "docker-compose workdir is resolved to real container path before proxy exec" { - # Regression: _lsp_exec_proxy was called with LINT_WORKDIR still set to - # the lazy sentinel "(resolved at call time)" in docker-compose mode, - # poisoning lsp_proxy.py's --container-root and breaking URI rewriting. - # lsp_run_or_null_stub must call _compose_resolve_workdir and reassign - # LINT_WORKDIR to the real path before dispatching. - _write_config '{"environment":"docker-compose","docker-compose":{"service":"web","workdir":"/var/www/html"},"enabled":true,"binary":"phpactor"}' - run bash -c " -$(_bootstrap_preamble) -source '${SHARED_DIR}/lsp_bootstrap.sh' -_lsp_preflight_container_binary() { return 0; } -_compose_resolve_workdir() { echo '/resolved/container/path'; } -wrap_command() { echo \"stub \$1\"; } -# Observe LINT_WORKDIR at the moment the proxy would be exec'd. -_lsp_exec_proxy() { echo \"observed_container_root=\${LINT_WORKDIR}\"; exit 0; } -lsp_run_or_null_stub 'phpactor language-server' -" - [ "$status" -eq 0 ] - [[ "$output" == *"observed_container_root=/resolved/container/path"* ]] - [[ "$output" != *"(resolved at call time)"* ]] -} - -@test "docker-compose workdir resolution failure falls back to null stub" { - # If _compose_resolve_workdir fails (e.g. service not running), the - # dispatcher must route to the null stub rather than passing a broken - # workdir to the proxy. - _write_config '{"environment":"docker-compose","docker-compose":{"service":"web","workdir":"/var/www/html"},"enabled":true,"binary":"phpactor"}' - run bash -c " -$(_bootstrap_preamble) -source '${SHARED_DIR}/lsp_bootstrap.sh' -_lsp_preflight_container_binary() { return 0; } -_compose_resolve_workdir() { echo 'simulated resolve error'; return 1; } -wrap_command() { echo \"stub \$1\"; } -lsp_run_or_null_stub 'phpactor language-server' -" - [ "$status" -eq 0 ] - [[ "$output" == *"target=null-stub"* ]] - [[ "$output" == *"failed to resolve docker-compose workdir"* ]] -} - -@test "preflight does not consume bytes from the LSP client stdin pipe" { - # Regression: _lsp_preflight_container_binary used \`docker exec -i ...\` - # which inherits the script's stdin. Claude Code writes the initialize - # request into the LSP pipe eagerly, and docker-exec-i would siphon those - # bytes into the preflight subprocess, which discards them — causing the - # real LSP server to wait forever for bytes that were already consumed. - # Preflight must redirect its stdin from /dev/null. - # - # We simulate docker-exec-i's stdin-eating behavior with a plain 'cat' - # in the wrap_command override: cat reads until EOF. If preflight runs - # without "${TMPDIR_PROJECT}/.claude/.lsp-php-tooling.json" - run bash -c " -$(_bootstrap_preamble) -source '${SHARED_DIR}/lsp_bootstrap.sh' -lsp_run_or_null_stub 'phpactor language-server' -" - [ "$status" -eq 0 ] - # Malformed JSON -> jq returns an error -> our `|| echo "false"` fallback - # kicks in, LSP_ENABLED becomes "false", dispatcher routes to null stub. - [[ "$output" == *"target=null-stub"* ]] -} diff --git a/plugin-tests/dev-tooling/lsp_null.bats b/plugin-tests/dev-tooling/lsp_null.bats deleted file mode 100644 index 0efc4658..00000000 --- a/plugin-tests/dev-tooling/lsp_null.bats +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bats -# bats file_tags=dev-tooling,lsp,null-stub -bats_require_minimum_version 1.11.0 - -# Regression tests for shared/lsp_null.sh — the minimal LSP stub used when -# an LSP is disabled or the dispatcher preflight fails. The stub must: -# - Respond to `initialize` with empty capabilities -# - Respond to `shutdown` with null result -# - Exit 0 on `exit` notification -# - Respond to any unknown REQUEST (has id) with JSON-RPC -32601 Method not found -# - Silently drop unknown NOTIFICATIONS (no id) -# - Preserve the id's JSON type (number vs string) in responses - -setup() { - STUB="$(cd "${BATS_TEST_DIRNAME}/../../plugins/dev-tooling/shared" && pwd)/lsp_null.sh" -} - -# Build an LSP frame for the given JSON body on stdout. -# Computes Content-Length from the body's byte length so tests don't hardcode -# fragile numeric header values that desynchronize when bodies change. -_frame() { - local body="$1" - local len - len=$(printf '%s' "$body" | wc -c | tr -d ' ') - printf 'Content-Length: %d\r\n\r\n%s' "$len" "$body" -} - -@test "initialize returns empty capabilities with preserved numeric id" { - local body='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' - run bash -c "$(declare -f _frame); _frame '$body' | bash '$STUB'" - [ "$status" -eq 0 ] - [[ "$output" == *'{"jsonrpc":"2.0","id":1,"result":{"capabilities":{}}}'* ]] -} - -@test "shutdown returns null result with preserved id" { - run bash -c " - { - $(declare -f _frame) - _frame '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}' - _frame '{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"shutdown\"}' - _frame '{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}' - } | bash '$STUB' - " - [ "$status" -eq 0 ] - [[ "$output" == *'{"jsonrpc":"2.0","id":2,"result":null}'* ]] -} - -@test "exit notification terminates cleanly with no extra output after initialize response" { - run bash -c " - { - $(declare -f _frame) - _frame '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{}}' - _frame '{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}' - } | bash '$STUB' - " - [ "$status" -eq 0 ] - # Only the initialize response frame should be present. - frame_count=$(printf '%s' "$output" | grep -c '^Content-Length:' || true) - [ "$frame_count" -eq 1 ] -} - -@test "unknown request with numeric id returns MethodNotFound with preserved id" { - local body='{"jsonrpc":"2.0","id":42,"method":"textDocument/documentSymbol","params":{}}' - run bash -c "$(declare -f _frame); _frame '$body' | bash '$STUB'" - [ "$status" -eq 0 ] - [[ "$output" == *'"id":42'* ]] - [[ "$output" == *'"code":-32601'* ]] - [[ "$output" == *'"message":"Method not found"'* ]] -} - -@test "unknown request with string id returns MethodNotFound with preserved id" { - local body='{"jsonrpc":"2.0","id":"req-abc","method":"textDocument/hover","params":{}}' - run bash -c "$(declare -f _frame); _frame '$body' | bash '$STUB'" - [ "$status" -eq 0 ] - [[ "$output" == *'"id":"req-abc"'* ]] - [[ "$output" == *'"code":-32601'* ]] -} - -@test "unknown notification (no id) produces no response and exits 0 on EOF" { - local body='{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{}}' - run bash -c "$(declare -f _frame); _frame '$body' | bash '$STUB'" - [ "$status" -eq 0 ] - [ -z "$output" ] -} diff --git a/plugin-tests/dev-tooling/lsp_proxy/conftest.py b/plugin-tests/dev-tooling/lsp_proxy/conftest.py deleted file mode 100644 index 4237a116..00000000 --- a/plugin-tests/dev-tooling/lsp_proxy/conftest.py +++ /dev/null @@ -1,106 +0,0 @@ -"""Shared fixtures for lsp_proxy tests. - -`fake_lsp_child_script` returns a path to a Python script that acts as a -minimal LSP child for the proxy to drive. The script reads frames from its -own stdin and replies predictably so pump-loop tests can verify the proxy's -full-duplex behavior without needing a real language server. - -If the `LSP_CHILD_LOG` env var is set, the fake child appends each received -frame body (as raw bytes, newline-separated) to that file. Tests use this to -verify what the proxy delivered to the child — i.e. to observe the result of -host→container URI rewriting on the forward path. -""" - -from pathlib import Path - -import pytest - -FAKE_CHILD_SOURCE = '''\ -#!/usr/bin/env python3 -"""Minimal LSP child for proxy tests. - -Reads frames from stdin and replies based on the method: -- initialize -> empty capabilities, echoes id -- shutdown -> null result, echoes id -- exit -> exits 0 -- echo/ -> response with `result: ` (for request tracing) -- Everything else -> MethodNotFound if id present, drop if notification - -Intentionally single-threaded and synchronous. Good enough for tests; not a -real LSP implementation. -""" -from __future__ import annotations -import json -import os -import sys - - -def read_frame() -> bytes | None: - content_length = None - while True: - line = sys.stdin.buffer.readline() - if not line: - return None - line = line.rstrip(b"\\r\\n") - if line == b"": - break - if line.lower().startswith(b"content-length:"): - content_length = int(line.split(b":", 1)[1].strip()) - if content_length is None: - return None - return sys.stdin.buffer.read(content_length) - - -def write_frame(body: bytes) -> None: - header = f"Content-Length: {len(body)}\\r\\n\\r\\n".encode("ascii") - sys.stdout.buffer.write(header) - sys.stdout.buffer.write(body) - sys.stdout.buffer.flush() - - -def main() -> int: - log_path = os.environ.get("LSP_CHILD_LOG") - while True: - raw = read_frame() - if raw is None: - return 0 - if log_path: - with open(log_path, "ab") as f: - f.write(raw + b"\\n") - msg = json.loads(raw.decode("utf-8")) - method = msg.get("method", "") - msg_id = msg.get("id") - - if method == "initialize": - resp = {"jsonrpc": "2.0", "id": msg_id, "result": {"capabilities": {}}} - write_frame(json.dumps(resp, separators=(",", ":")).encode("utf-8")) - elif method == "shutdown": - resp = {"jsonrpc": "2.0", "id": msg_id, "result": None} - write_frame(json.dumps(resp, separators=(",", ":")).encode("utf-8")) - elif method == "exit": - return 0 - elif method.startswith("echo/"): - resp = {"jsonrpc": "2.0", "id": msg_id, "result": raw.decode("utf-8")} - write_frame(json.dumps(resp, separators=(",", ":")).encode("utf-8")) - elif msg_id is not None: - resp = { - "jsonrpc": "2.0", - "id": msg_id, - "error": {"code": -32601, "message": "Method not found"}, - } - write_frame(json.dumps(resp, separators=(",", ":")).encode("utf-8")) - # Else: notification, dropped. - - -if __name__ == "__main__": - sys.exit(main()) -''' - - -@pytest.fixture -def fake_lsp_child_script(tmp_path: Path) -> Path: - """Write the fake-child script to a temp path and return it.""" - script = tmp_path / "fake_lsp_child.py" - script.write_text(FAKE_CHILD_SOURCE) - script.chmod(0o755) - return script diff --git a/plugin-tests/dev-tooling/lsp_proxy/test_error_paths.py b/plugin-tests/dev-tooling/lsp_proxy/test_error_paths.py deleted file mode 100644 index 365abbf1..00000000 --- a/plugin-tests/dev-tooling/lsp_proxy/test_error_paths.py +++ /dev/null @@ -1,53 +0,0 @@ -"""Error-path tests for lsp_proxy.""" - -import asyncio -import io -import sys -from pathlib import Path - -import pytest - -PROXY_DIR = Path(__file__).resolve().parents[3] / "plugins/dev-tooling/shared" -sys.path.insert(0, str(PROXY_DIR)) - -import lsp_proxy # noqa: E402 - -HOST = "/Users/dev/Software/shopware/shopware" -CONTAINER = "/var/www/html" - - -@pytest.mark.asyncio -async def test_spawn_failure_returns_one(): - stdin_reader = asyncio.StreamReader() - stdin_reader.feed_eof() - stdout_buf = io.BytesIO() - - result = await lsp_proxy.run( - host_root=HOST, - container_root=CONTAINER, - wrapper="/definitely/not/a/real/binary", - stdin_reader=stdin_reader, - stdout_writer=stdout_buf, - ) - - assert result == 1 - - -@pytest.mark.asyncio -async def test_malformed_content_length_raises(): - reader = asyncio.StreamReader() - reader.feed_data(b"Content-Length: not-a-number\r\n\r\n{}") - reader.feed_eof() - - with pytest.raises(lsp_proxy.FramingError, match="Invalid Content-Length header"): - await lsp_proxy.read_frame(reader) - - -@pytest.mark.asyncio -async def test_truncated_body_raises(): - reader = asyncio.StreamReader() - reader.feed_data(b"Content-Length: 100\r\n\r\nshort") - reader.feed_eof() - - with pytest.raises(lsp_proxy.FramingError, match=r"Truncated body: wanted 100 bytes, got 5"): - await lsp_proxy.read_frame(reader) diff --git a/plugin-tests/dev-tooling/lsp_proxy/test_framing.py b/plugin-tests/dev-tooling/lsp_proxy/test_framing.py deleted file mode 100644 index bb5dd09e..00000000 --- a/plugin-tests/dev-tooling/lsp_proxy/test_framing.py +++ /dev/null @@ -1,122 +0,0 @@ -"""Unit tests for LSP Content-Length frame read/write in lsp_proxy. - -LSP protocol framing (LSP spec §3): - Content-Length: \r\n - \r\n - - -The proxy must read and write this framing exactly. Body length is in bytes, -not characters — multi-byte UTF-8 matters. -""" - -import asyncio -import io -import sys -from pathlib import Path - -import pytest - -# Add the proxy module's directory to sys.path so we can import it directly. -PROXY_DIR = Path(__file__).resolve().parents[3] / "plugins/dev-tooling/shared" -sys.path.insert(0, str(PROXY_DIR)) - -import lsp_proxy # noqa: E402 - - -async def _stream_reader_from_bytes(data: bytes) -> asyncio.StreamReader: - """Build an asyncio.StreamReader populated with `data` and marked EOF.""" - reader = asyncio.StreamReader() - reader.feed_data(data) - reader.feed_eof() - return reader - - -@pytest.mark.asyncio -@pytest.mark.parametrize( - "body", - [ - pytest.param( - b'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}', - id="ascii", - ), - pytest.param( - b'{"jsonrpc":"2.0","method":"log","params":"line1\\nline2"}', - id="body_contains_newline", - ), - pytest.param( - '{"jsonrpc":"2.0","method":"log","params":"a — b"}'.encode(), - id="multibyte_utf8", - ), - ], -) -async def test_read_frame_returns_body_verbatim(body: bytes): - frame = b"Content-Length: %d\r\n\r\n%s" % (len(body), body) - reader = await _stream_reader_from_bytes(frame) - - result = await lsp_proxy.read_frame(reader) - - assert result == body - - -@pytest.mark.asyncio -async def test_read_frame_consecutive_frames(): - body1 = b'{"id":1}' - body2 = b'{"id":2}' - frame1 = b"Content-Length: %d\r\n\r\n%s" % (len(body1), body1) - frame2 = b"Content-Length: %d\r\n\r\n%s" % (len(body2), body2) - reader = await _stream_reader_from_bytes(frame1 + frame2) - - assert await lsp_proxy.read_frame(reader) == body1 - assert await lsp_proxy.read_frame(reader) == body2 - - -@pytest.mark.asyncio -async def test_read_frame_returns_none_on_eof(): - reader = await _stream_reader_from_bytes(b"") - - result = await lsp_proxy.read_frame(reader) - - assert result is None - - -@pytest.mark.asyncio -async def test_read_frame_raises_on_missing_content_length(): - reader = await _stream_reader_from_bytes(b"Foo: bar\r\n\r\n{}") - - with pytest.raises(lsp_proxy.FramingError, match="missing required Content-Length"): - await lsp_proxy.read_frame(reader) - - -def test_write_frame_builds_correct_header_ascii(): - buf = io.BytesIO() - body = b'{"jsonrpc":"2.0","id":1,"result":null}' - - lsp_proxy.write_frame(buf, body) - - expected = b"Content-Length: %d\r\n\r\n%s" % (len(body), body) - assert buf.getvalue() == expected - - -def test_write_frame_uses_byte_length_not_char_length_for_multibyte(): - buf = io.BytesIO() - body = '{"msg":"a — b"}'.encode() # em-dash is 3 bytes - - lsp_proxy.write_frame(buf, body) - - result = buf.getvalue() - assert result.startswith(b"Content-Length: %d\r\n\r\n" % len(body)) - assert result.endswith(body) - - -@pytest.mark.asyncio -async def test_roundtrip_multiple_frames(): - """Write two frames with different lengths, read them back, compare.""" - bodies = [b'{"id":1}', b'{"id":2,"params":{"deep":"value"}}'] - out = io.BytesIO() - for body in bodies: - lsp_proxy.write_frame(out, body) - - reader = await _stream_reader_from_bytes(out.getvalue()) - - for expected in bodies: - assert await lsp_proxy.read_frame(reader) == expected diff --git a/plugin-tests/dev-tooling/lsp_proxy/test_lifecycle.py b/plugin-tests/dev-tooling/lsp_proxy/test_lifecycle.py deleted file mode 100644 index b3ed5b33..00000000 --- a/plugin-tests/dev-tooling/lsp_proxy/test_lifecycle.py +++ /dev/null @@ -1,124 +0,0 @@ -"""Lifecycle tests for lsp_proxy: subprocess spawn, full-duplex pump, shutdown.""" - -import asyncio -import io -import json -import sys -from pathlib import Path - -import pytest - -PROXY_DIR = Path(__file__).resolve().parents[3] / "plugins/dev-tooling/shared" -sys.path.insert(0, str(PROXY_DIR)) - -import lsp_proxy # noqa: E402 - -HOST = "/Users/dev/Software/shopware/shopware" -CONTAINER = "/var/www/html" - - -def _frame(body: bytes) -> bytes: - return b"Content-Length: %d\r\n\r\n%s" % (len(body), body) - - -async def _drive_proxy( - fake_child_script: Path, - input_frames: bytes, -) -> tuple[bytes, int]: - """Run lsp_proxy.run() with its stdin/stdout rewired to in-memory buffers. - - Returns (output_bytes, exit_code) where exit_code is the real return - value from `lsp_proxy.run()`. - """ - stdin_reader = asyncio.StreamReader() - stdin_reader.feed_data(input_frames) - stdin_reader.feed_eof() - - stdout_buf = io.BytesIO() - - # The child command wraps the fake LSP child script. - wrapper = f"{sys.executable} {fake_child_script}" - - exit_code = await lsp_proxy.run( - host_root=HOST, - container_root=CONTAINER, - wrapper=wrapper, - stdin_reader=stdin_reader, - stdout_writer=stdout_buf, - ) - - return stdout_buf.getvalue(), exit_code - - -@pytest.mark.asyncio -async def test_initialize_roundtrip_through_proxy(fake_lsp_child_script: Path): - init_body = b'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' - exit_body = b'{"jsonrpc":"2.0","method":"exit"}' - - output, _ = await _drive_proxy(fake_lsp_child_script, _frame(init_body) + _frame(exit_body)) - - assert b'"id":1' in output - assert b'"capabilities":{}' in output - - -@pytest.mark.asyncio -async def test_host_to_container_rewrite_reaches_child( - fake_lsp_child_script: Path, tmp_path: Path, monkeypatch: pytest.MonkeyPatch -): - """Forward-path verification: the child must receive the container URI. - - Asserts directly on what the child observed via LSP_CHILD_LOG, not on the - round-trip output (which looks the same whether forward rewriting worked - or not, because the return path flips back). - """ - child_log = tmp_path / "child_received.log" - monkeypatch.setenv("LSP_CHILD_LOG", str(child_log)) - - host_uri = f"file://{HOST}/src/Kernel.php" - container_uri = f"file://{CONTAINER}/src/Kernel.php" - request_body = json.dumps( - {"jsonrpc": "2.0", "id": 42, "method": "echo/uri", "params": {"uri": host_uri}} - ).encode("utf-8") - exit_body = b'{"jsonrpc":"2.0","method":"exit"}' - - await _drive_proxy(fake_lsp_child_script, _frame(request_body) + _frame(exit_body)) - - received = child_log.read_bytes() - assert container_uri.encode("utf-8") in received - assert host_uri.encode("utf-8") not in received - - -@pytest.mark.asyncio -async def test_container_to_host_rewrite_on_return_path( - fake_lsp_child_script: Path, -): - """Return-path verification: the proxy's stdout must contain only host URIs. - - The fake child echoes the body it received (which contains container URIs - after the forward rewrite) back as a string. The proxy's container→host - rewrite flips those container URIs back to host URIs in the final output. - """ - host_uri = f"file://{HOST}/src/Kernel.php" - request_body = json.dumps( - {"jsonrpc": "2.0", "id": 42, "method": "echo/uri", "params": {"uri": host_uri}} - ).encode("utf-8") - exit_body = b'{"jsonrpc":"2.0","method":"exit"}' - - output, _ = await _drive_proxy(fake_lsp_child_script, _frame(request_body) + _frame(exit_body)) - - assert host_uri.encode("utf-8") in output - assert f"file://{CONTAINER}".encode() not in output - - -@pytest.mark.asyncio -async def test_clean_shutdown_on_exit_notification(fake_lsp_child_script: Path): - init_body = b'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' - exit_body = b'{"jsonrpc":"2.0","method":"exit"}' - - output, exit_code = await _drive_proxy( - fake_lsp_child_script, _frame(init_body) + _frame(exit_body) - ) - - assert exit_code == 0 - # Only the initialize response should appear; exit is a notification. - assert output.count(b"Content-Length:") == 1 diff --git a/plugin-tests/dev-tooling/lsp_proxy/test_uri_rewrite.py b/plugin-tests/dev-tooling/lsp_proxy/test_uri_rewrite.py deleted file mode 100644 index 11d64cb7..00000000 --- a/plugin-tests/dev-tooling/lsp_proxy/test_uri_rewrite.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Unit tests for URI rewriting in lsp_proxy. - -The rewrite is a raw byte-level string substitution on the serialized JSON -body. It deliberately does not parse JSON — any `file://` prefix -is replaced with `file://` and vice versa. Because the prefix -is long and specific, false positives are effectively impossible. -""" - -import sys -from pathlib import Path - -PROXY_DIR = Path(__file__).resolve().parents[3] / "plugins/dev-tooling/shared" -sys.path.insert(0, str(PROXY_DIR)) - -import lsp_proxy # noqa: E402 - -HOST = "/Users/dev/Software/shopware/shopware" -CONTAINER = "/var/www/html" - - -def test_host_to_container_rewrites_didopen_uri(): - body = ( - b'{"jsonrpc":"2.0","method":"textDocument/didOpen","params":' - b'{"textDocument":{"uri":"file:///Users/dev/Software/shopware/shopware/src/Kernel.php"}}}' - ) - - result = lsp_proxy.rewrite_uris_host_to_container(body, HOST, CONTAINER) - - assert b"file:///var/www/html/src/Kernel.php" in result - assert b"file:///Users/dev" not in result - - -def test_container_to_host_rewrites_publishdiagnostics_uri(): - body = ( - b'{"jsonrpc":"2.0","method":"textDocument/publishDiagnostics","params":' - b'{"uri":"file:///var/www/html/src/Kernel.php","diagnostics":[]}}' - ) - - result = lsp_proxy.rewrite_uris_container_to_host(body, HOST, CONTAINER) - - assert b"file:///Users/dev/Software/shopware/shopware/src/Kernel.php" in result - assert b"file:///var/www/html/src" not in result - - -def test_roundtrip_host_to_container_to_host_is_identity(): - original = b'{"uri":"file:///Users/dev/Software/shopware/shopware/src/Kernel.php"}' - - forward = lsp_proxy.rewrite_uris_host_to_container(original, HOST, CONTAINER) - back = lsp_proxy.rewrite_uris_container_to_host(forward, HOST, CONTAINER) - - assert back == original - - -def test_rewrite_handles_multiple_uris_in_one_message(): - body = ( - b'{"result":[{"uri":"file:///Users/dev/Software/shopware/shopware/a.php"},' - b'{"uri":"file:///Users/dev/Software/shopware/shopware/b.php"}]}' - ) - - result = lsp_proxy.rewrite_uris_host_to_container(body, HOST, CONTAINER) - - assert result.count(b"file:///var/www/html/") == 2 - assert b"/Users/dev/" not in result - - -def test_rewrite_leaves_non_matching_uris_alone(): - body = b'{"uri":"file:///tmp/unrelated.txt"}' - - result = lsp_proxy.rewrite_uris_host_to_container(body, HOST, CONTAINER) - - assert result == body - - -def test_rewrite_leaves_non_file_uris_alone(): - body = b'{"uri":"https://example.com/docs","url":"git://foo"}' - - result = lsp_proxy.rewrite_uris_host_to_container(body, HOST, CONTAINER) - - assert result == body - - -def test_rewrite_preserves_multibyte_utf8_elsewhere_in_body(): - # The substitution must not decode/re-encode the whole body, so non-ASCII - # bytes in unrelated fields must pass through unchanged. - em_dash = "—".encode() # 3 bytes - body = ( - b'{"uri":"file:///Users/dev/Software/shopware/shopware/src/Kernel.php",' - b'"message":"hello ' + em_dash + b' world"}' - ) - - result = lsp_proxy.rewrite_uris_host_to_container(body, HOST, CONTAINER) - - assert em_dash in result - assert b"/var/www/html/src/Kernel.php" in result - - -def test_rewrite_is_byte_exact_not_path_aware(): - # If a field happens to contain the exact host-root prefix but isn't a - # file:// URI, it will NOT be rewritten because the substitution looks - # for the "file://" prefix. - body = b'{"cwd":"/Users/dev/Software/shopware/shopware"}' - - result = lsp_proxy.rewrite_uris_host_to_container(body, HOST, CONTAINER) - - assert result == body # cwd field left alone diff --git a/plugin-tests/mcp-shared/config.bats b/plugin-tests/mcp-shared/config.bats index 35a18b57..dc438b2a 100644 --- a/plugin-tests/mcp-shared/config.bats +++ b/plugin-tests/mcp-shared/config.bats @@ -1,8 +1,8 @@ #!/usr/bin/env bats # bats file_tags=mcp-core,config -# Tests for the shared config module's filename and environment-variable prefix -# parameterization: the MCP path (.mcp-.json, MCP_*_CONFIG), the LSP path -# (.lsp-.json, LSP_*_CONFIG), and the two required-input refusals. +# Tests for the shared config module's filename and environment-variable +# derivation from CONFIG_PREFIX (.mcp-.json, MCP_*_CONFIG) and its +# required-input refusal. # Sources the template source of truth (templates/mcp-shared/config.sh); every # plugin copy is kept byte-identical to it by the template-sync CI check, so this # one suite covers the module in all consuming plugins. @@ -14,13 +14,12 @@ setup() { CONFIG_SH="${REPO_ROOT}/templates/mcp-shared/config.sh" } -@test "MCP path: CONFIG_FILE_NAME is .mcp-php-tooling.json when CONFIG_FILE_PREFIX unset" { +@test "CONFIG_FILE_NAME is .mcp-php-tooling.json and CONFIG_ENV_VAR is MCP_PHP_TOOLING_CONFIG" { run bash -c " set -euo pipefail log() { :; } export -f log CONFIG_PREFIX='php-tooling' - unset CONFIG_FILE_PREFIX CONFIG_ENV_VAR_PREFIX source '${CONFIG_SH}' printf '%s|%s\n' \"\${CONFIG_FILE_NAME}\" \"\${CONFIG_ENV_VAR}\" " @@ -28,41 +27,12 @@ setup() { assert_line --index -1 ".mcp-php-tooling.json|MCP_PHP_TOOLING_CONFIG" } -@test "LSP path: CONFIG_FILE_NAME is .lsp-php-tooling.json when CONFIG_FILE_PREFIX=.lsp-" { +@test "Missing CONFIG_PREFIX fails" { run bash -c " set -euo pipefail log() { :; } export -f log - CONFIG_PREFIX='php-tooling' - CONFIG_FILE_PREFIX='.lsp-' - CONFIG_ENV_VAR_PREFIX='LSP' - source '${CONFIG_SH}' - printf '%s|%s\n' \"\${CONFIG_FILE_NAME}\" \"\${CONFIG_ENV_VAR}\" - " - assert_success - assert_line --index -1 ".lsp-php-tooling.json|LSP_PHP_TOOLING_CONFIG" -} - -@test "LSP path fails when CONFIG_FILE_PREFIX set but CONFIG_ENV_VAR_PREFIX unset" { - run bash -c " - set -euo pipefail - log() { :; } - export -f log - CONFIG_PREFIX='php-tooling' - CONFIG_FILE_PREFIX='.lsp-' - unset CONFIG_ENV_VAR_PREFIX - source '${CONFIG_SH}' - " - assert_failure - assert_output --partial "CONFIG_ENV_VAR_PREFIX required" -} - -@test "Missing CONFIG_PREFIX fails on the MCP path" { - run bash -c " - set -euo pipefail - log() { :; } - export -f log - unset CONFIG_PREFIX CONFIG_FILE_PREFIX CONFIG_ENV_VAR_PREFIX + unset CONFIG_PREFIX source '${CONFIG_SH}' " assert_failure diff --git a/plugins/dev-tooling/.claude-plugin/plugin.json b/plugins/dev-tooling/.claude-plugin/plugin.json index 04d61fcf..91ce8d93 100644 --- a/plugins/dev-tooling/.claude-plugin/plugin.json +++ b/plugins/dev-tooling/.claude-plugin/plugin.json @@ -1,12 +1,12 @@ { "name": "dev-tooling", - "version": "4.1.1", - "description": "Three MCP servers for PHP and JavaScript operations plus an optional PHP language server. PHP (php-tooling): PHPStan, ECS, Rector, PHPUnit, PHPUnit coverage gap analysis, Symfony Console. Administration (js-admin-tooling): ESLint, Stylelint, Prettier, Jest, TypeScript, Vite builds. Storefront (js-storefront-tooling): ESLint, Stylelint, Jest, Webpack builds. LSP (opt-in): phpactor for PHP, routed through a Python URI-rewriting proxy for containerized environments. Includes SessionStart hook that injects MCP tool directives and PreToolUse hooks that enforce MCP tool usage by blocking bash commands. Supports native, Docker, Docker Compose, Vagrant and DDEV environments.", + "version": "5.0.0", + "description": "Three MCP servers for PHP and JavaScript operations. PHP (php-tooling): PHPStan, ECS, Rector, PHPUnit, PHPUnit coverage gap analysis, Symfony Console. Administration (js-admin-tooling): ESLint, Stylelint, Prettier, Jest, TypeScript, Vite builds. Storefront (js-storefront-tooling): ESLint, Stylelint, Jest, Webpack builds. Includes SessionStart hook that injects MCP tool directives and PreToolUse hooks that enforce MCP tool usage by blocking bash commands. Supports native, Docker, Docker Compose, Vagrant and DDEV environments.", "author": { "name": "Shopware Labs" }, "license": "MIT", - "keywords": ["php", "javascript", "phpstan", "ecs", "rector", "phpunit", "eslint", "stylelint", "prettier", "jest", "typescript", "vite", "webpack", "mcp", "hooks", "static-analysis", "testing", "code-quality", "linting", "lsp", "shopware", "twig", "language-server"], + "keywords": ["php", "javascript", "phpstan", "ecs", "rector", "phpunit", "eslint", "stylelint", "prettier", "jest", "typescript", "vite", "webpack", "mcp", "hooks", "static-analysis", "testing", "code-quality", "linting", "shopware", "twig"], "homepage": "https://github.com/shopwareLabs/ai-coding-tools/tree/main/plugins/dev-tooling", "repository": "https://github.com/shopwareLabs/ai-coding-tools" } diff --git a/plugins/dev-tooling/.codex-plugin/plugin.json b/plugins/dev-tooling/.codex-plugin/plugin.json index c263b55e..3f6bdb4c 100644 --- a/plugins/dev-tooling/.codex-plugin/plugin.json +++ b/plugins/dev-tooling/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dev-tooling", - "version": "4.1.1", + "version": "5.0.0", "description": "Three MCP servers for PHP and JavaScript operations. PHP (php-tooling): PHPStan, ECS, Rector, PHPUnit, PHPUnit coverage gap analysis, Symfony Console. Administration (js-admin-tooling): ESLint, Stylelint, Prettier, Jest, TypeScript, Vite builds. Storefront (js-storefront-tooling): ESLint, Stylelint, Jest, Webpack builds. Includes SessionStart hook that injects MCP tool directives and PreToolUse hooks that enforce MCP tool usage by blocking bash commands. Supports native, Docker, Docker Compose, Vagrant and DDEV environments.", "author": { "name": "Shopware Labs" diff --git a/plugins/dev-tooling/.lsp.json b/plugins/dev-tooling/.lsp.json deleted file mode 100644 index acbf5c7d..00000000 --- a/plugins/dev-tooling/.lsp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "phpactor": { - "command": "${CLAUDE_PLUGIN_ROOT}/lsp-server-php/lsp.sh", - "extensionToLanguage": { - ".php": "php" - } - } -} diff --git a/plugins/dev-tooling/AGENTS.md b/plugins/dev-tooling/AGENTS.md index 577027be..ce42e6ae 100644 --- a/plugins/dev-tooling/AGENTS.md +++ b/plugins/dev-tooling/AGENTS.md @@ -9,7 +9,6 @@ plugins/dev-tooling/ ├── docs/ # User-facing documentation │ ├── configuration.md # Config files, environments, troubleshooting │ ├── mcp-enforcement.md # Hook enforcement, blocked commands, plugin integration -│ ├── lsp.md # LSP setup, phpactor limitations, troubleshooting │ └── reference.md # Full tool parameter docs and examples ├── AGENTS.md # LLM navigation guide (this file) ├── CLAUDE.md # Points to AGENTS.md @@ -18,7 +17,6 @@ plugins/dev-tooling/ ├── .codex-plugin/ # Codex plugin manifest (plugin.json: same name and version; mcpServers points at codex.mcp.json) ├── .mcp.json # MCP server registration for Claude Code (php-tooling, js-admin-tooling, js-storefront-tooling) ├── codex.mcp.json # MCP server registration for Codex: same three servers, each launched with `cwd` "." relative to the plugin root -├── .lsp.json # LSP server configuration (phpactor PHP LSP; Claude Code only) │ ├── agents/ # AGENTS (dev-tooling check/fix executor) │ └── dev-tooling-runner.md # Lean runner; given targets + checks/fixes, runs them and returns a pass/fail report (haiku) @@ -27,12 +25,9 @@ plugins/dev-tooling/ │ ├── hooks.json # Hook configuration (SessionStart + PreToolUse + PostToolUse) │ ├── prompts/ │ │ ├── mcp-tool-directives.md # SessionStart prompt for Claude Code: MCP tool listing and usage rules -│ │ ├── mcp-tool-directives-codex.md # SessionStart prompt for Codex: the same tool listing, without the runner agent and EnterWorktree/ExitWorktree guidance -│ │ ├── lsp-directives-header.md # SessionStart prompt: LSP preamble, emitted when an LSP is enabled -│ │ └── lsp-directives-php.md # SessionStart prompt: phpactor tool listing and usage rules +│ │ └── mcp-tool-directives-codex.md # SessionStart prompt for Codex: the same tool listing, without the runner agent and EnterWorktree/ExitWorktree guidance │ └── scripts/ │ ├── session-start.sh # SessionStart hook: picks the prompt file by host, checks enforcement, outputs JSON -│ ├── lsp-directives.sh # SessionStart hook: emits the LSP directives when .lsp-php-tooling.json enables one │ ├── check-php-tools.sh # Blocks PHPStan, ECS, PHPUnit, Rector, bin/console bash commands │ ├── check-js-admin-tools.sh # Blocks Administration npm/npx commands (ESLint, Stylelint, Prettier, Jest, TSC, Vite) │ ├── check-js-storefront-tools.sh # Blocks Storefront npm/npx/composer commands (ESLint, Stylelint, Jest, Vitest, ludtwig, Webpack) @@ -52,16 +47,8 @@ plugins/dev-tooling/ │ ├── js_paths.sh # JS path-scoped runs: local-binary probe, directory patterns, ddev-worktree refusal, Stylelint config detector (dev-tooling-owned, not templated) │ ├── scope.sh # Scope resolution: resolve_scope(), scope_get_tool_field() │ ├── docker-compose.sh # Docker Compose environment: call-time resolution of container/workdir -│ ├── lsp_bootstrap.sh # LSP entry point: picks phpactor or the null stub from .lsp-php-tooling.json -│ ├── lsp_null.sh # LSP null stub used when no LSP is enabled -│ ├── lsp_proxy.py # URI-rewriting LSP proxy for container-hosted phpactor │ └── mcp-js-tooling.schema.json # JSON Schema for .mcp-js-tooling.json (shared by JS servers) │ -├── lsp-server-php/ # PHP LSP SERVER (opt-in) -│ ├── lsp.sh # Entry point from .lsp.json; sets CONFIG_PREFIX/LSP_DEFAULT_BINARY, sources the bootstrap -│ └── lib/ -│ └── phpactor.sh # Per-LSP launcher: adjusts LSP_BINARY / args for phpactor -│ ├── mcp-server-php/ # PHP TOOLS MCP SERVER │ ├── server.sh # Entry point - sets CONFIG_PREFIX="php-tooling" │ ├── config.json # Server metadata (name="php-tooling") @@ -111,17 +98,11 @@ This plugin provides: - `php-tooling` - PHP linting/testing tools - `js-admin-tooling` - Administration JavaScript tools (Vue 3/Vite) - `js-storefront-tooling` - Storefront JavaScript tools (vanilla JS/Webpack): `eslint_check`, `eslint_fix`, `stylelint_check`, `stylelint_fix`, `jest_run`, `vitest_run`, `ludtwig_check`, `ludtwig_fix`, `webpack_build`, `worktree_prepare` -- **Codex packaging**: `.codex-plugin/plugin.json` (same name, version and other shared metadata as the Claude Code manifest, but a description and keywords without the Claude Code only components; `mcpServers` pointing at `codex.mcp.json` and no `hooks` field, so Codex loads `hooks/hooks.json` itself) plus, at the repository root, `.agents/plugins/marketplace.json`, which lists `dev-tooling` as an available local plugin for `codex plugin marketplace add`. Codex reads that marketplace before `.claude-plugin/marketplace.json` and resolves a plugin's `.codex-plugin/plugin.json` before `.claude-plugin/plugin.json`. `codex.mcp.json` repeats the three servers with a relative `command` and `cwd` `.`, because Codex does not expand `${CLAUDE_PLUGIN_ROOT}`, and a Codex MCP server receives only a small default environment (`HOME`, `PATH`, `SHELL`, `USER`, `TMPDIR` and a few more) plus the variables its entry declares — so each entry lists what to forward: `PROJECT_ROOT`, the server's `MCP_*_TOOLING_CONFIG` override, `MCP_LOG_STDERR`, `MCP_EXTRA_LOG_FILE`, the `DOCKER_*` set, the docker compose selectors `COMPOSE_PROJECT_NAME`, `COMPOSE_FILE`, `COMPOSE_PROFILES` and `COMPOSE_PATH_SEPARATOR`, the Vagrant settings `VAGRANT_CWD`, `VAGRANT_HOME`, `VAGRANT_DOTFILE_PATH`, `VAGRANT_VAGRANTFILE` and `VAGRANT_DEFAULT_PROVIDER`, and `SSH_AUTH_SOCK`. -- **PHP LSP (phpactor, opt-in)** via `.lsp.json` (Claude Code only — Codex reads no `.lsp.json`): - - Active PHP code discovery: document symbols, hover, go-to-definition, references - - Runs natively on the host or inside a container (docker, docker-compose, vagrant, ddev) via the URI-rewriting proxy - - Enabled via `.lsp-php-tooling.json` with `enabled: true`; falls back to the null stub otherwise - - Requires the `phpactor` binary available where the LSP runs (host or container) +- **Codex packaging**: `.codex-plugin/plugin.json` (same name, version, description, keywords and other shared metadata as the Claude Code manifest; `mcpServers` pointing at `codex.mcp.json` and no `hooks` field, so Codex loads `hooks/hooks.json` itself) plus, at the repository root, `.agents/plugins/marketplace.json`, which lists `dev-tooling` as an available local plugin for `codex plugin marketplace add`. Codex reads that marketplace before `.claude-plugin/marketplace.json` and resolves a plugin's `.codex-plugin/plugin.json` before `.claude-plugin/plugin.json`. `codex.mcp.json` repeats the three servers with a relative `command` and `cwd` `.`, because Codex does not expand `${CLAUDE_PLUGIN_ROOT}`, and a Codex MCP server receives only a small default environment (`HOME`, `PATH`, `SHELL`, `USER`, `TMPDIR` and a few more) plus the variables its entry declares — so each entry lists what to forward: `PROJECT_ROOT`, the server's `MCP_*_TOOLING_CONFIG` override, `MCP_LOG_STDERR`, `MCP_EXTRA_LOG_FILE`, the `DOCKER_*` set, the docker compose selectors `COMPOSE_PROJECT_NAME`, `COMPOSE_FILE`, `COMPOSE_PROFILES` and `COMPOSE_PATH_SEPARATOR`, the Vagrant settings `VAGRANT_CWD`, `VAGRANT_HOME`, `VAGRANT_DOTFILE_PATH`, `VAGRANT_VAGRANTFILE` and `VAGRANT_DEFAULT_PROVIDER`, and `SSH_AUTH_SOCK`. - **Subagent** via `agents/` (Claude Code only — the Codex plugin manifest has no agents field): - `dev-tooling-runner` — executor for dev-tooling checks (and rule-driven fixes); run it to keep verbose output out of the conversation and get back a lean pass/fail report (runs on haiku); see [Agents](#agents) -- **SessionStart Hooks** via `hooks/hooks.json`: +- **SessionStart Hook** via `hooks/hooks.json`: - `session-start.sh` injects MCP tool directives into conversation context at session start; prompt maintained in `hooks/prompts/mcp-tool-directives.md` when `CLAUDE_PROJECT_DIR` is set and in `hooks/prompts/mcp-tool-directives-codex.md` when it is not; outputs JSON `additionalContext` format; the Claude Code prompt also steers the active session to delegate heavy dev-tool runs to `dev-tooling-runner`, the Codex prompt does not. When `enforce_mcp_tools` is off but the host names the session directory only in the hook input's `.cwd` (Codex sets no `CLAUDE_PROJECT_DIR`), it still injects a short hint naming the directory and telling the model to call `set_project_root` on a server that reports no project root - - `lsp-directives.sh` injects LSP usage directives, only when `.lsp-php-tooling.json` enables a language server (Claude Code only) - **PreToolUse Hooks** via `hooks/hooks.json`: - Blocks bash commands that should use MCP tools instead; the block message names the tool and its server, e.g. ``Use `phpstan_analyze` on the `php-tooling` MCP server instead!``, so it reads the same on Claude Code and Codex, which spell the tool name differently - PHP hook: blocks PHPStan, ECS, PHPUnit, Rector, bin/console @@ -131,7 +112,7 @@ This plugin provides: - `check-phpstan-baseline.sh` warns when a targeted `phpstan_analyze` run covers paths listed in `phpstan-baseline.neon` (or `.php`). Its matcher carries both host spellings of the tool name — `mcp__plugin_dev-tooling_php-tooling__phpstan_analyze` on Claude Code and `mcp__php_tooling__phpstan_analyze` on Codex, which replaces the dashes in the server name - `worktree-directives.sh` fires on `EnterWorktree`/`ExitWorktree` and reminds the session to call `set_project_root` on all three servers; on enter it also names the repair command when the worktree's `.git` file carries an absolute gitdir pointer, and warns that a native worktree branches from the default remote branch unless `worktree.baseRef` is `head` (Claude Code only — those tools do not exist on Codex) - Both ignore `enforce_mcp_tools` and always run -- `session-start.sh` and every PreToolUse hook are configurable via `enforce_mcp_tools: false` in config files; `lsp-directives.sh` and both PostToolUse hooks read no such flag +- `session-start.sh` and every PreToolUse hook are configurable via `enforce_mcp_tools: false` in config files; both PostToolUse hooks read no such flag - **Shared Framework** in `shared/` - reusable across all servers ## 🤖 Agents @@ -244,7 +225,7 @@ Both handle environment-specific execution (native/docker/docker-compose/vagrant | Modify protocol | upstream `shopwareLabs/bash-mcp-sdk` (release pinned in `.mcp-sdk.lock`; never edit `shared/mcpserver_core.sh` in place) | - | `process_request()`, `handle_*()` | | Update tool schemas | `mcp-server-*/tools.json` | - | JSON Schema Draft 7 | | Register new server | `.mcp.json` for Claude Code; `codex.mcp.json` for Codex | - | `mcpServers` object; the Codex file needs a relative `command` and `cwd` `.` | -| Package the plugin for Codex | `.codex-plugin/plugin.json` | `codex.mcp.json`, repo `.agents/plugins/marketplace.json` | name, version and shared metadata equal to `.claude-plugin/plugin.json`, description and keywords differing (equality of the rest asserted by `codex_manifest.bats`, which also asserts the description and keywords omit the LSP), no `hooks` field | +| Package the plugin for Codex | `.codex-plugin/plugin.json` | `codex.mcp.json`, repo `.agents/plugins/marketplace.json` | name, version, description, keywords and the other shared metadata equal to `.claude-plugin/plugin.json` (equality asserted by `codex_manifest.bats`), no `hooks` field | | Change launch or bind behavior | `shared/launch.sh` | `shared/worktree.sh` (`worktree_launch_hydrate`, `_worktree_launch_bind`) | `LAUNCH_MODE`, the `launch` record in the state file, `worktree_state_cleanup` removing the merged config | ## ✏️ When to Modify What @@ -321,8 +302,6 @@ This plugin's own suites are in `plugin-tests/dev-tooling/`: | `scope_php_tools.bats` | Scope handling in the PHP MCP tools | | `scope_js_tools.bats` | Scope handling in the JS MCP tools | | `scope_session_start.bats` | Scope surfacing in the SessionStart output | -| `lsp_bootstrap.bats` | LSP bootstrap: binary preflight, direct vs proxy dispatch | -| `lsp_null.bats` | LSP null stub protocol behavior | | `worktree_resolution.bats` | Worktree resolution, identity, linkage, charset, probe, config, deps, path guard | | `worktree_state.bats` | State file: sticky read-back, reset, a removed root, probe cache, atomic writes | | `worktree_hook.bats` | `worktree-directives.sh`: the `PostToolUse` JSON envelope, enter/exit directive text read out of the decoded `additionalContext`, the `.cwd` fallback, the no-path diagnostic, exit 0 on malformed input | @@ -330,9 +309,9 @@ This plugin's own suites are in `plugin-tests/dev-tooling/`: | `worktree_js_tools.bats` | `project_root` reaching the JS package directory, which the conformance scan does not capture | | `worktree_conformance.bats` | Every enumerated `tool_*` function runs in the named worktree and runs nothing against a refused one, with the enumeration reconciled against `tools.json` | | `codex_launch.bats` | Launch mode and the bind: a server started in its plugin directory lists tools and refuses every other call, `set_project_root` binds it (exercised under `native`, `docker-compose` and `ddev`) and refuses a bad root, `cwd` reports both states, and a server started in a project directory runs a tool there with no bind | -| `codex_manifest.bats` | Codex packaging: `.codex-plugin/plugin.json` version and shared-metadata equality with the Claude Code manifest and a description and keywords free of the LSP, `codex.mcp.json` server launch and forwarded variables, the `.agents/plugins/marketplace.json` entry, and that both directive prompts (Claude Code and Codex) list each server's `tools.json` tools and name a server at every tool mention; the Codex prompt carries no runner-agent or `EnterWorktree`/`ExitWorktree` guidance | +| `codex_manifest.bats` | Codex packaging: `.codex-plugin/plugin.json` version, description, keywords and shared-metadata equality with the Claude Code manifest, `codex.mcp.json` server launch and forwarded variables, the `.agents/plugins/marketplace.json` entry, and that both directive prompts (Claude Code and Codex) list each server's `tools.json` tools and name a server at every tool mention; the Codex prompt carries no runner-agent or `EnterWorktree`/`ExitWorktree` guidance | -Four non-suite entries sit alongside them: +Three non-suite entries sit alongside them: - `test_helper/common_setup.bash`: shared helpers. - `setup_config()` and `setup()`. @@ -343,16 +322,15 @@ Four non-suite entries sit alongside them: - `js_execute_in_env()`, which runs a command a JS tool built through `wrap_npm_command` for one environment against fake `npm`, `docker`, `vagrant` and `ddev` and fake linter binaries. - `fixtures/coverage/`: Clover XML samples for `mcp_tool_phpunit_coverage.bats`. - `fixtures/shopware-trunk/`: the `scripts` blocks of the Administration and Storefront `package.json` files, copied verbatim from shopware/shopware trunk. The JS tool suites answer script probes from them. -- `lsp_proxy/`: the Python `pytest` suite for `shared/lsp_proxy.py`, run outside BATS. The modules this plugin consumes from `templates/mcp-shared/` are covered once, for every consuming plugin, in `plugin-tests/mcp-shared/`: -| Test File | Coverage | +| Test File | Coverage | |-------------------------------|---------------------------------------------------------------------------| | `environment.bats` | Environment wrapping, argument quoting, `parse_paths_json`, path guards | | `docker_compose.bats` | Docker Compose call-time container/workdir resolution | | `scope_wrap.bats` | Scope-aware command wrapping per environment | -| `config.bats` | Config filename and env-var prefix parameterization, including `.lsp-` | +| `config.bats` | Config filename and env-var derivation from `CONFIG_PREFIX` | Run tests: ```bash diff --git a/plugins/dev-tooling/CHANGELOG.md b/plugins/dev-tooling/CHANGELOG.md index 3e00b247..12785362 100644 --- a/plugins/dev-tooling/CHANGELOG.md +++ b/plugins/dev-tooling/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [5.0.0] - 2026-10-11 + +### Removed +- **The optional phpactor PHP language server is gone.** The plugin no longer registers `lsp-server-php` (`.lsp.json`), so Claude Code gets no document symbols, hover, go-to-definition or references for PHP from it. PHPStan through the `php-tooling` MCP server stays. The LSP launcher scripts, the Python URI-rewriting proxy and `docs/lsp.md` are deleted. +- **The LSP SessionStart directive is gone.** `hooks/scripts/lsp-directives.sh` and its two prompts are deleted and `hooks/hooks.json` runs `session-start.sh` alone. The plugin ignores `.lsp-php-tooling.json` and `LSP_PHP_TOOLING_CONFIG`; delete the file if you created one. +- **`python3` 3.12 and `phpactor` are no longer prerequisites.** Both were needed only for the LSP. The `dev-tooling` setup skill drops its `.lsp-php-tooling.json` questions, its phpactor install recipe and its LSP validation steps. + ## [4.1.1] - 2026-10-11 ### Changed diff --git a/plugins/dev-tooling/README.md b/plugins/dev-tooling/README.md index e9d70a18..36ec0089 100644 --- a/plugins/dev-tooling/README.md +++ b/plugins/dev-tooling/README.md @@ -1,6 +1,6 @@ # Dev Tooling -PHP and JavaScript tooling for Shopware 6 exposed through three MCP servers, plus an optional PHP language server (phpactor) for active code discovery. Wraps the toolchain you already run on the command line: PHPStan, ECS, PHPUnit, Rector, Symfony Console, ESLint, Stylelint, Prettier, Jest, Vitest, ludtwig, TypeScript, and the Vite and Webpack builds. Works against native installs, Docker, Docker Compose, Vagrant, and DDEV, with the environment auto-detected from your config. Runs on Claude Code and on OpenAI Codex; the LSP, the runner agent, and the worktree reminder hook are Claude Code only. +PHP and JavaScript tooling for Shopware 6 exposed through three MCP servers. Wraps the toolchain you already run on the command line: PHPStan, ECS, PHPUnit, Rector, Symfony Console, ESLint, Stylelint, Prettier, Jest, Vitest, ludtwig, TypeScript, and the Vite and Webpack builds. Works against native installs, Docker, Docker Compose, Vagrant, and DDEV, with the environment auto-detected from your config. Runs on Claude Code and on OpenAI Codex; the runner agent and the worktree reminder hook are Claude Code only. ## 🧩 Features @@ -47,10 +47,6 @@ PHP and JavaScript tooling for Shopware 6 exposed through three MCP servers, plu All three servers read a single JSON config per language (`.mcp-php-tooling.json`, `.mcp-js-tooling.json`) and discover it from the project root or any of the common AI-tool config directories (`.claude/`, `.cursor/`, `.windsurf/`, `.zed/`, `.cline/`, `.aiassistant/`, `.amazonq/`, `.kiro/`). Multiple files are deep-merged so you can commit a base config and layer a personal override on top. Every command is wrapped for the declared environment (native, docker, docker-compose, vagrant, ddev). Known runtime noise such as Xdebug Step Debug connection failures is stripped from tool output before it reaches Claude, which keeps results clean without hiding actual errors. -### LSP Support (opt-in) - -Optional Language Server Protocol integration for active PHP code discovery through [phpactor](https://github.com/phpactor/phpactor). See [docs/lsp.md](./docs/lsp.md) for installation, phpactor limitations, and troubleshooting. - ## ⚡ Quick Start ### Installation (Claude Code) @@ -82,7 +78,7 @@ codex plugin add dev-tooling@shopware-ai-coding-tools Codex runs plugin hooks only once you trust them. Its startup review and the `/hooks` command both record trust in your Codex user config, and a non-interactive `codex exec` runs the trusted hooks without asking, or all of them with `--dangerously-bypass-hook-trust`. An untrusted hook is skipped, so MCP tool enforcement and the PHPStan baseline warning stay off until you trust them. > [!NOTE] -> Three parts are Claude Code only: the phpactor LSP bridge (`.lsp.json`), the `dev-tooling-runner` subagent (Codex has no plugin agents), and the `EnterWorktree`/`ExitWorktree` reminder hook (those tools are Claude Code's). +> Two parts are Claude Code only: the `dev-tooling-runner` subagent (Codex has no plugin agents) and the `EnterWorktree`/`ExitWorktree` reminder hook (those tools are Claude Code's). ### Interactive Setup @@ -174,16 +170,12 @@ Use scopes when developing a Shopware plugin inside `custom/plugins//`. A Run the `setting-up` skill to let Claude probe the plugin and write the scope for you. -> [!NOTE] -> Scopes apply to MCP tools only, not to the LSP. phpactor always indexes the project root declared in `.lsp-php-tooling.json`. See [docs/lsp.md](./docs/lsp.md#-scopes-and-the-lsp) for the rationale. - ## 📚 Documentation The plugin docs are split by concern so the README stays scannable: - [docs/configuration.md](./docs/configuration.md) covers config files, discovery priority, environment options, dependencies, and troubleshooting. - [docs/mcp-enforcement.md](./docs/mcp-enforcement.md) explains the hook layer, how to turn it off, which bash commands get redirected, and how other plugins integrate with these tools. -- [docs/lsp.md](./docs/lsp.md) walks through the opt-in LSP setup, the known phpactor limitations, and container cleanup. - [docs/reference.md](./docs/reference.md) is the full tool parameter reference. ## ⚖️ License diff --git a/plugins/dev-tooling/SETUP.md b/plugins/dev-tooling/SETUP.md index c6a5ccbb..5f003db8 100644 --- a/plugins/dev-tooling/SETUP.md +++ b/plugins/dev-tooling/SETUP.md @@ -6,7 +6,6 @@ - [Configuration Files](#configuration-files) - [.mcp-php-tooling.json](#mcp-php-toolingjson) - [.mcp-js-tooling.json](#mcp-js-toolingjson) - - [.lsp-php-tooling.json](#lsp-php-toolingjson) - [Plugin Scope Setup](#plugin-scope-setup) - [Permission Groups](#permission-groups) - [Validation](#validation) @@ -21,62 +20,6 @@ - **Install**: https://jqlang.github.io/jq/download/ - **Required by**: All three MCP servers (php-tooling, js-admin-tooling, js-storefront-tooling) -### python3 -- **Check**: `python3 --version` -- **Install**: Usually pre-installed on macOS (Monterey+) and Linux. If missing: - - macOS: `brew install python@3.12` - - Debian/Ubuntu: `apt install python3` -- **Required by**: Only for PHP LSP support when using a containerized `environment` (docker, docker-compose, vagrant, ddev). Not required for native LSP or when LSP is disabled. Must be version 3.12 or newer. - -### phpactor (optional) -- **Check**: `phpactor --version` -- **Install (native/host)**: Download the phar release from https://github.com/phpactor/phpactor/releases/latest into `~/.local/bin/phpactor` (or anywhere on `$PATH`), then `chmod +x` it. Alternative: `composer global require phpactor/phpactor`. See https://phpactor.readthedocs.io/en/master/usage/standalone.html for details. Homebrew has no phpactor formula — do not attempt `brew install`. -- **Install (containerized)**: See the phar-sidecar recipe below. Do NOT use `composer require --dev phpactor/phpactor` in the project's `composer.json` for third-party repos like `shopware/shopware` — it pollutes the committed dependency list and pulls phpactor's transitive dependencies into the project's vendor tree. -- **Required by**: Optional PHP LSP integration (document symbols, hover, go-to-definition, references). The MCP servers work without it. For containerized environments the binary must be available inside the container; the host check can be skipped. - -#### Containerized install — phar sidecar (recommended for docker-compose) - -Use when the PHP service's base image does not ship phpactor (e.g. `ghcr.io/shopware/docker-dev`). A one-shot sidecar downloads the phar into a named volume; the PHP service mounts the volume read-only. No image rebuild, no `composer.json` pollution. The named volume persists across `docker compose down`; only `down -v` drops it and forces a re-download. - -Create `compose.override.yaml` next to the project's `compose.yaml`: - -```yaml -services: - phpactor-installer: - image: alpine/curl - restart: "no" - command: - - sh - - -c - - | - set -e - if [ ! -x /out/phpactor ]; then - curl -fsSL -o /out/phpactor \ - https://github.com/phpactor/phpactor/releases/latest/download/phpactor.phar - chmod +x /out/phpactor - fi - volumes: - - phpactor-bin:/out - - web: - depends_on: - phpactor-installer: - condition: service_completed_successfully - environment: - PHPACTOR_UNCONDITIONAL_TRUST: "1" - volumes: - - phpactor-bin:/opt/phpactor:ro - -volumes: - phpactor-bin: -``` - -Notes: -- Replace `web` with the actual service name if your stack differs. -- Binary path inside the container: `/opt/phpactor/phpactor`. Use this for the LSP binary-path question (question 10). -- `PHPACTOR_UNCONDITIONAL_TRUST=1` silences phpactor's per-project trust prompt. Without it, local `.phpactor.json` is ignored until you run `phpactor config:trust --trust` inside the container, and a fresh container re-prompts. The env var is the right default for containerized dev. -- Verify with `docker compose exec /opt/phpactor/phpactor --version` before running the LSP dispatcher check. - ## Configuration Files ### Scope of this setup @@ -212,69 +155,6 @@ Stored as `enforce_mcp_tools` in the config. } ``` -### .lsp-php-tooling.json - -**Required:** No — only create this file if you want PHP LSP support (phpactor). - -**Location:** Project root or any supported tool directory (`.claude/`, `.cursor/`, etc.) — same discovery rules as `.mcp-php-tooling.json`. Plugin subdirectories are not scanned. - -**Prerequisite binary:** `phpactor` must be installed where the LSP will run: -- Native: on your host — install the phar release (see the phpactor prerequisite section above) -- Containerized: inside the container — use the phar-sidecar recipe from the phpactor prerequisite section. For projects where adding phpactor to the committed dev dependencies is acceptable, `composer require --dev phpactor/phpactor` also works. - -#### Setup Questions - -1. **Do you want PHP LSP support enabled?** - - Yes → continue with this section - - No → skip this section entirely - -2. **Where should the LSP run?** - - `native` → runs on your host; file URIs pass through unchanged, no Python required - - `docker` / `docker-compose` / `ddev` / `vagrant` → runs inside the container; requires `python3` ≥ 3.12 on the host and `phpactor` inside the container - -3. **Docker container name** (only if environment = docker): Container name from `docker ps`, e.g. `shopware_app`. - -4. **Docker working directory** (only if environment = docker, optional): Default `/var/www/html`. - -5. **Compose service name** (only if environment = docker-compose): Which service in your `docker-compose.yml` runs phpactor? Default: `web` - -6. **Compose working directory** (only if environment = docker-compose, optional): Working directory inside the compose service. Leave blank to use the service's default `WORKDIR`. - -7. **Compose file** (only if environment = docker-compose, optional): Path to a specific compose file. Leave blank to auto-discover from the project root. - -8. **Vagrant working directory** (only if environment = vagrant, optional): Default `/vagrant`. - -9. **DDEV working directory** (only if environment = ddev, optional): Default `/var/www/html`. - -10. **Binary path (optional)** - - Leave blank to look up `phpactor` in `$PATH` (works for native installs and for containerized setups where phpactor is on the container's `$PATH`). - - Containerized + phar-sidecar install: `/opt/phpactor/phpactor`. This matches the mount target from the recipe in the phpactor prerequisite section. - - Or provide any other absolute path. - -#### Minimal Config - -Native: - -```json -{ - "environment": "native", - "enabled": true -} -``` - -Containerized (docker-compose, matches a typical Shopware setup): - -```json -{ - "environment": "docker-compose", - "docker-compose": { - "service": "web", - "workdir": "/var/www/html" - }, - "enabled": true -} -``` - ## Plugin Scope Setup Optional phase. Writes one or more scopes into `.mcp-php-tooling.json` and/or `.mcp-js-tooling.json` and optionally pins one as `default_scope` when the user develops against a Shopware plugin in `custom/plugins//`. @@ -376,9 +256,7 @@ Validation runs in two stages. Stage 1 checks config-file existence and shape be - `.mcp-php-tooling.json` exists at a valid discovery location, parses as JSON, and matches the schema. - `.mcp-js-tooling.json` (if created) exists, parses, matches schema. -- `.lsp-php-tooling.json` (if created) exists, parses, matches schema. - Every scope has a `cwd` pointing at an existing directory relative to the project root. -- Dry-run the LSP dispatcher as described under "PHP LSP" below (does not require MCP server restart). ### Stage 2 — Post-restart (live MCP dispatch) @@ -401,54 +279,8 @@ Run each applicable check below. Skip any whose config file was not created. If - **Pass**: ESLint output with results - **Fail**: Connection error or "command not found" error -#### PHP LSP (only if .lsp-php-tooling.json was created) - -Step 1 — binary reachability: -- Native: run `phpactor --version` on the host (or the absolute path configured in `.lsp-php-tooling.json`). -- Containerized: run the equivalent inside the container, e.g. `docker compose exec /opt/phpactor/phpactor --version` for the phar-sidecar recipe. -- **Pass**: prints a version line. -- **Fail**: binary missing or not at the configured path. Fix the install before the dispatcher check: re-check the sidecar volume mount or the container's `$PATH`; run `docker compose up -d` if the sidecar has not completed. - -Step 2 — dispatcher dry-run against the current project: - -```bash -LSP_DISPATCH_DRY_RUN=1 PROJECT_ROOT="$(pwd)" \ - bash "${CLAUDE_SKILL_DIR}/../../lsp-server-php/lsp.sh" -``` - -- **Pass**: output contains `target=direct-exec` (native) or `target=python-proxy` (containerized). The dispatcher resolved config, enabled flag, environment, and the phpactor preflight. -- **Fail**: output contains `target=null-stub reason=...`. Read the reason: - - `no .lsp-php-tooling.json found` — config missing or unreadable - - `enabled=false` — flip `enabled` to `true` - - `preflight failed: phpactor not found in context` — install phpactor inside the container (or on the host for native) - - `unsupported environment` — environment value is not one of `native|docker|docker-compose|vagrant|ddev` - - `failed to resolve docker-compose workdir` — the compose service can't be resolved; check `service`, `file`, and that the stack is up -- Note: this check validates dispatch only. Live LSP behavior (document symbols, hover, etc.) can only be exercised by opening a `.php` file in Claude Code after restart. The `Method not found from plugin:dev-tooling:phpactor` error at runtime means the null stub was selected — rerun the dry-run check to diagnose. - ## Post-Setup - Restart Claude Code after creating configuration files. The MCP servers are loaded at startup and will not pick up new config files until restart. - If you change a configuration file later, you also need to restart Claude Code. - After restart, the dev-tooling MCP tools will appear in your tool list. You can verify by asking Claude to run PHPStan on a file. - -### Containerized LSP lifecycle - -If you enabled an LSP with a containerized `environment` (docker, docker-compose, vagrant, ddev), **the container must be running before you open Claude Code**. Language servers start lazily — Claude Code spawns the LSP process the first time a matching file is opened in the session. If the container is down at that moment, the dispatcher's preflight check fails, falls back to the null stub, and the session continues without LSP support until you quit and restart Claude Code. - -Practical recipe: - -1. `docker compose up -d` (or `ddev start`, etc.) -2. `claude` (start Claude Code) -3. Open a file to trigger LSP lazy spawn - -If you ever see `Method not found from plugin:dev-tooling:phpactor` when you expected real LSP behavior, the most likely cause is the container wasn't up at startup. Restart Claude Code after starting the container. - -### Project-root `.phpactor.json` side effect - -phpactor may write a `.phpactor.json` file into the project root on first LSP use to persist its own session state. For a project you own, commit it or add it to the repo's `.gitignore` as you see fit. For third-party repos such as `shopware/shopware` where you do not want to touch the committed `.gitignore` or any shared git configuration, add the file to your local clone's exclude list: - -```bash -echo ".phpactor.json" >> .git/info/exclude -``` - -`.git/info/exclude` is per-clone, never committed, and uses the same syntax as `.gitignore`. `git status` will stop reporting the file without affecting any other developer's workflow. diff --git a/plugins/dev-tooling/docs/lsp.md b/plugins/dev-tooling/docs/lsp.md deleted file mode 100644 index a834fc62..00000000 --- a/plugins/dev-tooling/docs/lsp.md +++ /dev/null @@ -1,62 +0,0 @@ -# LSP Support - -The dev-tooling plugin ships an optional Language Server Protocol bridge for PHP, powered by [phpactor](https://github.com/phpactor/phpactor) (MIT, pure PHP). With it enabled, Claude Code can call `documentSymbol`, `hover`, `goToDefinition`, `findReferences`, and diagnostics as regular tool invocations instead of having to grep or read large swaths of code to locate symbols. - -Containerized environments get a little extra machinery. A stdlib-only Python proxy (`shared/lsp_proxy.py`) sits between Claude Code and phpactor and rewrites `file://` URIs on every LSP frame, so the language server inside the container sees paths that exist on its filesystem. Before spawning a containerized LSP, the dispatcher preflight-checks that the binary is actually present. If it isn't, it falls back to a minimal null stub so Claude doesn't crash-loop against a missing server. - -> [!NOTE] -> Claude Code only issues LSP tool calls when `ENABLE_LSP_TOOL=1` is set in its environment (typically under `env` in `~/.claude/settings.json`). Without the flag, LSP diagnostics still surface passively in context, but the agent can't invoke LSP operations directly. -> -> The bridge itself is Claude Code only: Codex reads no `.lsp.json`, so nothing on this page applies there and a `.lsp-php-tooling.json` has no effect. - -## 📦 Installation - -LSP support is opt-in. Create `.lsp-php-tooling.json` with `"enabled": true` to turn it on. The `setting-up` skill can walk you through this interactively if you prefer. - -Install [phpactor](https://phpactor.readthedocs.io/) wherever the LSP will actually run: on the host for `native`, inside the container for `docker`, `docker-compose`, `ddev`, or `vagrant`. - -```bash -# host -brew install phpactor - -# containerized -composer require --dev phpactor/phpactor -``` - -Minimal `.lsp-php-tooling.json` for docker-compose: - -```json -{ - "environment": "docker-compose", - "docker-compose": { "service": "web", "workdir": "/var/www/html" }, - "enabled": true -} -``` - -Containerized LSPs also need Python 3.12+ on the host `PATH` for the URI-rewriting proxy. It uses the standard library only, so there are no packages to install. Native mode execs phpactor directly and doesn't touch Python at all. - -## 🚫 phpactor Limitations - -Claude Code exposes nine LSP operations; phpactor implements six of them. What works: `documentSymbol`, `workspaceSymbol`, `hover`, `goToDefinition`, `goToImplementation`, and `findReferences`. What's missing: the call-hierarchy trio (`prepareCallHierarchy`, `incomingCalls`, `outgoingCalls`). Phpactor has no handler for them, so walk call chains manually with `findReferences` plus `goToDefinition`. - -There's one known bug worth flagging. `workspaceSymbol` currently caps results at 250 and ignores the query string, so it returns the first 250 symbols no matter what you ask for. Prefer `documentSymbol` on a specific file, or fall back to Grep for a workspace-wide identifier search. - -Cold-start latency is also something to know about. The first LSP request against a PHP file takes 10 to 30 seconds while phpactor parses it and resolves its dependencies. Subsequent requests against the same file are fast. There's no cache warmup today. - -These limits are injected as SessionStart context via `hooks/scripts/lsp-directives.sh`, so Claude avoids the unsupported operations automatically when the PHP LSP is enabled in your project. - -## 🧭 Scopes and the LSP - -MCP tool scopes (`scopes` / `default_scope` in `.mcp-*-tooling.json`) do not apply to the LSP. phpactor always launches at the project root declared in `.lsp-php-tooling.json`, regardless of which scope the MCP tools are using. - -This is intentional. Plugin-scoped LSPs would primarily buy a smaller index and faster startup, not better symbol resolution: phpactor resolves through composer autoload, so `Shopware\Core\…` FQNs resolve correctly from a plugin root, and dynamic lookups (DI container IDs, event names, Twig refs) stay opaque either way. Scoping would also make cross-boundary navigation worse — jumping from a plugin file into core would land outside the indexed tree. The extra config surface isn't worth the trade-off unless someone reports that indexing a big monorepo is too slow in practice. - -If phpactor's cold-start or memory profile becomes a real issue on your project, file an issue with numbers and we'll revisit. - -## 🩺 Troubleshooting LSP - -**`Method not found from plugin:dev-tooling:phpactor`.** The dispatcher fell back to the null stub. Usual suspects: `enabled` is missing or set to `false` in the LSP config; the container wasn't running when Claude Code spawned the LSP (LSPs start lazily on the first matching file open, so either start the container before launching Claude Code, or restart Claude Code after bringing the container up); phpactor isn't installed inside the container; or `python3` isn't on the host `PATH` for containerized mode. - -**LSP doesn't load at all.** Check the `/plugin` Errors tab and confirm `ENABLE_LSP_TOOL=1` is set in your Claude Code environment. - -**LSP servers piling up inside the container.** If you run containerized LSPs and kill Claude Code (or let it time out on an in-flight tool call) without a clean LSP shutdown, the container-side phpactor process isn't reaped. This is a `docker exec -i` signal-propagation gap ([moby/moby#9098](https://github.com/moby/moby/issues/9098)): when the host-side exec process dies, the container-side child doesn't get signaled. Each new session spawns another server alongside the old ones, which can add up over a working day. Until the plugin handles this automatically, the options are to restart the service periodically (`docker compose restart `) or to kill the stale servers by hand (`docker compose exec pkill -f phpactor.phar`). Parallel Claude Code sessions on the same container are a valid pattern, so only blanket-kill phpactor processes when you're sure no other live session owns them. diff --git a/plugins/dev-tooling/docs/mcp-enforcement.md b/plugins/dev-tooling/docs/mcp-enforcement.md index 141126c7..0d683a0a 100644 --- a/plugins/dev-tooling/docs/mcp-enforcement.md +++ b/plugins/dev-tooling/docs/mcp-enforcement.md @@ -10,7 +10,7 @@ MCP is a synchronous request-response protocol. A long-running watcher like `npm Three hook events carry scripts between them: SessionStart, PreToolUse, and PostToolUse. -**SessionStart** runs two. The first injects a directive at the top of every conversation that lists the available MCP tools and tells Claude to prefer them over bash; the prompt lives in `hooks/prompts/mcp-tool-directives.md` if you want to read or tweak it. On a host that names the session directory only in the hook input rather than in `CLAUDE_PROJECT_DIR` — Codex — the directive comes from `hooks/prompts/mcp-tool-directives-codex.md` instead, which leaves out the runner-agent and `EnterWorktree`/`ExitWorktree` guidance Codex has no counterpart for, and the hook also appends a short hint naming that directory and the `set_project_root` call that binds a server which started without a project root. The second emits the LSP directives, and only when `.lsp-php-tooling.json` enables a language server (Claude Code only). +**SessionStart** runs one script. It injects a directive at the top of every conversation that lists the available MCP tools and tells Claude to prefer them over bash; the prompt lives in `hooks/prompts/mcp-tool-directives.md` if you want to read or tweak it. On a host that names the session directory only in the hook input rather than in `CLAUDE_PROJECT_DIR` — Codex — the directive comes from `hooks/prompts/mcp-tool-directives-codex.md` instead, which leaves out the runner-agent and `EnterWorktree`/`ExitWorktree` guidance Codex has no counterpart for, and the hook also appends a short hint naming that directory and the `set_project_root` call that binds a server which started without a project root. **PreToolUse** runs one script per server, and they are the safety net: they intercept bash commands that map to a known MCP tool and point Claude at the replacement, so even if the SessionStart directive got ignored or compacted away, the bad call gets caught before it runs. Each block message names the tool and its server — ``Use `phpstan_analyze` on the `php-tooling` MCP server instead!`` — which reads the same on Claude Code and on Codex, where the full tool name differs. @@ -19,7 +19,7 @@ Three hook events carry scripts between them: SessionStart, PreToolUse, and Post **PostToolUse** runs two. One watches `phpstan_analyze`: when it runs against specific files, it cross-references `phpstan-baseline.neon` (or `.php`) and surfaces a warning if any of the analyzed paths appear in the baseline, which usually means a baseline entry has gone stale. Full-project PHPStan runs skip the check because PHPStan validates the baseline natively there. Its matcher carries both host spellings of the tool name, so it fires for a Claude Code plugin install and for a Codex install alike. The other watches `EnterWorktree` and `ExitWorktree` — both Claude Code tools — and reminds the session to call `set_project_root` on all three servers, since each holds its own sticky root. -Within SessionStart, `session-start.sh` honors `enforce_mcp_tools` and turns off when it's `false`; the project-root hint above is not gated on the flag, because a server started without a project root refuses every tool whatever the flag says. `lsp-directives.sh` ignores the flag and runs whenever `.lsp-php-tooling.json` enables a language server. Every PreToolUse script honors the flag. Both PostToolUse scripts, the baseline check and the worktree-directives reminder, ignore the flag and always run. +The SessionStart script, `session-start.sh`, honors `enforce_mcp_tools` and turns off when it's `false`; the project-root hint above is not gated on the flag, because a server started without a project root refuses every tool whatever the flag says. Every PreToolUse script honors the flag. Both PostToolUse scripts, the baseline check and the worktree-directives reminder, ignore the flag and always run. On Codex, hooks run only once you trust them: its startup review and the `/hooks` command each record trust in your Codex user config, an untrusted hook is skipped, and a non-interactive `codex exec` runs the trusted hooks without asking or all of them with `--dangerously-bypass-hook-trust`. All of them are plugin hooks, so none runs there until you trust it. diff --git a/plugins/dev-tooling/hooks/hooks.json b/plugins/dev-tooling/hooks/hooks.json index 2d12ed19..630302ae 100644 --- a/plugins/dev-tooling/hooks/hooks.json +++ b/plugins/dev-tooling/hooks/hooks.json @@ -1,5 +1,5 @@ { - "description": "SessionStart hooks inject MCP tool and LSP usage directives. PreToolUse hooks enforce MCP dev tools over bash equivalents. PostToolUse hooks warn on stale PHPStan baseline entries and remind the session to re-point `set_project_root` after a worktree switch.", + "description": "SessionStart hook injects MCP tool usage directives. PreToolUse hooks enforce MCP dev tools over bash equivalents. PostToolUse hooks warn on stale PHPStan baseline entries and remind the session to re-point `set_project_root` after a worktree switch.", "hooks": { "SessionStart": [ { @@ -8,11 +8,6 @@ "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/session-start.sh", "timeout": 5 - }, - { - "type": "command", - "command": "${CLAUDE_PLUGIN_ROOT}/hooks/scripts/lsp-directives.sh", - "timeout": 5 } ] } diff --git a/plugins/dev-tooling/hooks/prompts/lsp-directives-header.md b/plugins/dev-tooling/hooks/prompts/lsp-directives-header.md deleted file mode 100644 index 09a82ac4..00000000 --- a/plugins/dev-tooling/hooks/prompts/lsp-directives-header.md +++ /dev/null @@ -1,21 +0,0 @@ -LSP, Grep, and Read each answer different questions. Pick by what the question is. - -Use LSP when symbol identity matters — type resolution excludes same-named methods on unrelated classes that Grep cannot distinguish: -- Rename safety and blast-radius counts: `findReferences` on the method declaration. -- "Who implements this interface method": `goToImplementation` in one call. -- Walking an inheritance chain: chained `goToDefinition` hops, one per parent, exact line numbers. -- Dead-code check on non-private methods. -- Vendor deprecation / upgrade audit: `findReferences` on a removed or deprecated vendor symbol returns the exact caller list across project and vendor code. - -Use Grep when the question is textual, scoped, or touches surfaces the LSP skips: -- String literals, config, comments, phpdoc, conventions, non-code files. -- Public API map with types and signatures — `Grep "public function"` carries what `documentSymbol` omits. -- Private methods — scope eliminates collisions, one call covers the file or directory. -- `.stub` files, PHPStan baselines, fixtures — the LSP skips all of these. -- Very common symbols (class names, framework hotspots) — LSP `findReferences` on these explodes. - -Use Read when you need the whole file cheaply: -- File under ~400 lines — one Read delivers more than two LSP calls and gives visibility, types, and phpdoc for free. -- You need visibility (public/private/protected) or phpdoc — `documentSymbol` omits visibility, `hover` is unreliable on project methods. - -NEVER run `findReferences` on a class declaration or on a widely-used vendor interface method (`LoggerInterface::info`, `Request::get`, `EventDispatcherInterface::dispatch`, anything on `ContainerInterface`). Either returns thousands of hits and blows the context budget. Scope `findReferences` to narrow or deprecated method/function declarations. diff --git a/plugins/dev-tooling/hooks/prompts/lsp-directives-php.md b/plugins/dev-tooling/hooks/prompts/lsp-directives-php.md deleted file mode 100644 index 3535d079..00000000 --- a/plugins/dev-tooling/hooks/prompts/lsp-directives-php.md +++ /dev/null @@ -1,17 +0,0 @@ -PHP LSP is available for `.php` files. - -Supported ops: `documentSymbol`, `workspaceSymbol`, `hover`, `goToDefinition`, `goToImplementation`, `findReferences`. - -NEVER call `prepareCallHierarchy`, `incomingCalls`, or `outgoingCalls`. The PHP LSP does not implement them; walk call chains manually with `findReferences` plus `goToDefinition`. - -Scope: project `src/` and `tests/`, the full `vendor/` tree, and PHP stdlib builtins. All ops work across the vendor boundary without additional setup. - -Known failure modes — assume them before trusting a result: - -- `hover` is asymmetric. Dense and reliable on vendor class references — returns the full class skeleton, constants, method signatures, implemented interfaces, and `@deprecated` flags in a single call, often better than reading vendor source. Unreliable on project methods — sometimes returns "No hover information available" on real public methods. Fall back to Read for project-code phpdoc. -- `documentSymbol` returns name, kind, and line only — no visibility, no types, no signatures. For public-API audits use `Grep "public function"` instead. -- `workspaceSymbol` caps at 250 results and ignores the query string. Effectively unusable; use `documentSymbol` on a known file, or Grep. -- `goToImplementation` returns empty when it cannot resolve the symbol. An empty result does not confirm "no implementations exist" — cross-check with Grep before concluding. -- `findReferences` skips `.stub` files, PHPStan baselines, fixtures, comments, and phpdoc. Follow up with Grep when rename safety depends on those surfaces. -- First request against a file can take 10–30 seconds; subsequent requests on the same file are fast. -- Returned paths may be rooted at a different filesystem view than the host (e.g. `/var/www/html/...` for containerized runs). Rebase before feeding them to Read or Grep. diff --git a/plugins/dev-tooling/hooks/scripts/lsp-directives.sh b/plugins/dev-tooling/hooks/scripts/lsp-directives.sh deleted file mode 100755 index 96e28674..00000000 --- a/plugins/dev-tooling/hooks/scripts/lsp-directives.sh +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env bash -# SessionStart hook: inject LSP usage directives into conversation context. -# Assembles the prompt from per-server sections based on which LSP configs -# are present and enabled in the project (.lsp-php-tooling.json). -# Exits silently if no LSP server is enabled. -set -euo pipefail - -# Claude Code writes hook-event JSON to stdin for every hook, including -# SessionStart; draining it avoids blocking the harness's write on a payload -# larger than the pipe buffer. -cat > /dev/null - -HOOK_DIR="$(cd "$(dirname "$0")/.." && pwd)" -PROMPT_DIR="${HOOK_DIR}/prompts" - -[[ -z "${CLAUDE_PROJECT_DIR:-}" ]] && exit 0 -command -v jq &>/dev/null || exit 0 - -# Return 0 if .lsp-.json exists in the project and has enabled=true. -is_lsp_enabled() { - local prefix="$1" - local config_file="" - for loc in ".claude/.lsp-${prefix}.json" ".lsp-${prefix}.json"; do - if [[ -f "${CLAUDE_PROJECT_DIR}/${loc}" ]]; then - config_file="${CLAUDE_PROJECT_DIR}/${loc}" - break - fi - done - [[ -z "$config_file" ]] && return 1 - - local enabled - enabled=$(jq -r '.enabled // false' "$config_file" 2>/dev/null || echo "false") - [[ "$enabled" == "true" ]] -} - -sections=() -is_lsp_enabled "php-tooling" && sections+=("${PROMPT_DIR}/lsp-directives-php.md") - -[[ ${#sections[@]} -eq 0 ]] && exit 0 - -# Assemble: header + one section per enabled server. -content="" -header="${PROMPT_DIR}/lsp-directives-header.md" -if [[ -f "$header" ]]; then - content+="$(cat "$header")"$'\n\n' -fi -for f in "${sections[@]}"; do - [[ -f "$f" ]] || continue - content+="$(cat "$f")"$'\n\n' -done - -[[ -z "$content" ]] && exit 0 - -additional_context=$(printf '%s' "$content" | jq -Rs '.') -cat </dev/null || true - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -SHARED_DIR="$(cd "${SCRIPT_DIR}/../shared" && pwd)" -PROJECT_ROOT="${PROJECT_ROOT:-$(pwd)}" - -# Required by shared/config.sh + shared/lsp_bootstrap.sh -CONFIG_PREFIX="php-tooling" -CONFIG_FILE_PREFIX=".lsp-" -CONFIG_ENV_VAR_PREFIX="LSP" -LSP_DEFAULT_BINARY="phpactor" - -export SCRIPT_DIR SHARED_DIR PROJECT_ROOT -export CONFIG_PREFIX CONFIG_FILE_PREFIX CONFIG_ENV_VAR_PREFIX LSP_DEFAULT_BINARY - -source "${SHARED_DIR}/lsp_bootstrap.sh" - -# Per-LSP launcher may adjust LSP_BINARY or append args. -source "${SCRIPT_DIR}/lib/phpactor.sh" - -lsp_run_or_null_stub "${LSP_BINARY} language-server" diff --git a/plugins/dev-tooling/shared/config.sh b/plugins/dev-tooling/shared/config.sh index e3550829..ed09871e 100644 --- a/plugins/dev-tooling/shared/config.sh +++ b/plugins/dev-tooling/shared/config.sh @@ -25,28 +25,10 @@ if [[ -z "${CONFIG_PREFIX:-}" ]]; then exit 1 fi -# Generate config file name and environment variable from prefix. -# -# MCP path (default, CONFIG_FILE_PREFIX unset): +# Generate config file name and environment variable from prefix: # php-tooling -> .mcp-php-tooling.json, MCP_PHP_TOOLING_CONFIG -# -# LSP path (caller sets CONFIG_FILE_PREFIX=".lsp-", CONFIG_ENV_VAR_PREFIX="LSP"): -# php-tooling -> .lsp-php-tooling.json, LSP_PHP_TOOLING_CONFIG -# -# The LSP path is explicit by design — we don't default CONFIG_FILE_PREFIX to ".mcp-" -# because a silent fallback would mask caller bugs. MCP's .mcp- literal stays hardcoded -# in the else branch below. -if [[ -n "${CONFIG_FILE_PREFIX:-}" ]]; then - if [[ -z "${CONFIG_ENV_VAR_PREFIX:-}" ]]; then - echo "ERROR: CONFIG_ENV_VAR_PREFIX required when CONFIG_FILE_PREFIX is set" >&2 - exit 1 - fi - CONFIG_FILE_NAME="${CONFIG_FILE_PREFIX}${CONFIG_PREFIX}.json" - CONFIG_ENV_VAR="${CONFIG_ENV_VAR_PREFIX}_${CONFIG_PREFIX^^}_CONFIG" -else - CONFIG_FILE_NAME=".mcp-${CONFIG_PREFIX}.json" - CONFIG_ENV_VAR="MCP_${CONFIG_PREFIX^^}_CONFIG" -fi +CONFIG_FILE_NAME=".mcp-${CONFIG_PREFIX}.json" +CONFIG_ENV_VAR="MCP_${CONFIG_PREFIX^^}_CONFIG" CONFIG_ENV_VAR="${CONFIG_ENV_VAR//-/_}" # Configuration locations relative to PROJECT_ROOT diff --git a/plugins/dev-tooling/shared/lsp_bootstrap.sh b/plugins/dev-tooling/shared/lsp_bootstrap.sh deleted file mode 100644 index d40fc6e2..00000000 --- a/plugins/dev-tooling/shared/lsp_bootstrap.sh +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env bash -# Common bootstrap for LSP dispatchers in lsp-server-/lsp.sh. -# -# Caller contract (must set before sourcing): -# - SCRIPT_DIR, SHARED_DIR, PROJECT_ROOT -# - CONFIG_PREFIX (e.g., "php-tooling") -# - CONFIG_FILE_PREFIX=".lsp-" -# - CONFIG_ENV_VAR_PREFIX="LSP" -# - LSP_DEFAULT_BINARY (e.g., "phpactor") -# -# After sourcing, exposes: -# - LINT_ENV, LINT_WORKDIR (from shared/environment.sh) -# - LSP_ENABLED ("true" or "false") -# - LSP_BINARY (from config, defaulting to LSP_DEFAULT_BINARY) -# - Function `lsp_run_or_null_stub ` -# -# The function exec's into one of three targets: -# 1. shared/lsp_null.sh (disabled, config missing, or preflight failed) -# 2. directly (environment: native) -# 3. python3 shared/lsp_proxy.py (containerized environment) -# -# Test hook: when LSP_DISPATCH_DRY_RUN is set, the function prints the -# target decision to stdout instead of exec'ing. Used by BATS tests. - -set -uo pipefail -shopt -s inherit_errexit 2>/dev/null || true - -: "${SCRIPT_DIR:?SCRIPT_DIR required}" -: "${SHARED_DIR:?SHARED_DIR required}" -: "${PROJECT_ROOT:?PROJECT_ROOT required}" -: "${CONFIG_PREFIX:?CONFIG_PREFIX required}" -: "${CONFIG_FILE_PREFIX:?CONFIG_FILE_PREFIX required}" -: "${CONFIG_ENV_VAR_PREFIX:?CONFIG_ENV_VAR_PREFIX required}" -: "${LSP_DEFAULT_BINARY:?LSP_DEFAULT_BINARY required}" - -# shared/config.sh needs `log` from mcpserver_core.sh. We don't want the full -# MCP server runtime loaded for an LSP, so provide a minimal log stub that -# writes to stderr. -if ! declare -f log >/dev/null 2>&1; then - log() { - local level="$1" - shift - printf '[lsp-%s] %s\n' "${level,,}" "$*" >&2 - } - export -f log -fi - -# Function definitions come BEFORE any `source` call that could fall back into -# them. Bash does not hoist functions, so defining them later would make the -# fallback paths in `source config.sh` and `load_config` fail with -# "command not found" at the exact moment the null stub is supposed to fire. - -_lsp_exec_null_stub() { - local reason="${1:-}" - if [[ -n "$reason" ]]; then - log "INFO" "dispatching to null stub: $reason" - fi - if [[ "${LSP_DISPATCH_DRY_RUN:-}" == "1" ]]; then - echo "target=null-stub reason=${reason:-unknown}" - exit 0 - fi - exec bash "${SHARED_DIR}/lsp_null.sh" -} - -_lsp_exec_direct() { - local cmd="$1" - - # Both refusals below are checked ahead of the dry-run branch, so a dry run - # reports the decision a real run would take. - # - # `read` consumes only the first line, so a line break would silently drop - # every word after it — and a line break in a `binary` config value is not a - # usable path, so there is nothing to recover. - case "${cmd}" in - *$'\n'*|*$'\r'*) - log "ERROR" "LSP binary command contains a line break: ${cmd}" - log "ERROR" "set a single-line 'binary' value in ${CONFIG_FILE_PREFIX}${CONFIG_PREFIX}.json" - exit 1 - ;; - esac - - # `read -r -a` splits on IFS without globbing, which a bare `exec ${cmd}` - # would not. - local -a cmd_arr=() - read -r -a cmd_arr <<< "${cmd}" - # `exec` with no arguments is a no-op returning 0, so an empty command - # would leave no language server running and still report success. - if [[ ${#cmd_arr[@]} -eq 0 ]]; then - log "ERROR" "LSP binary command is empty; no language server to exec" - log "ERROR" "set a non-empty 'binary' value in ${CONFIG_FILE_PREFIX}${CONFIG_PREFIX}.json" - exit 1 - fi - - if [[ "${LSP_DISPATCH_DRY_RUN:-}" == "1" ]]; then - echo "target=direct-exec cmd=${cmd}" - exit 0 - fi - - exec "${cmd_arr[@]}" -} - -_lsp_exec_proxy() { - local wrapped="$1" - if [[ "${LSP_DISPATCH_DRY_RUN:-}" == "1" ]]; then - echo "target=python-proxy wrapped=${wrapped}" - exit 0 - fi - if ! command -v python3 >/dev/null 2>&1; then - log "ERROR" "python3 not found on PATH; required for containerized LSP" - log "ERROR" "install python3 or change environment to 'native' in the LSP config" - exit 1 - fi - exec python3 "${SHARED_DIR}/lsp_proxy.py" \ - --host-root "${PROJECT_ROOT}" \ - --container-root "${LINT_WORKDIR}" \ - --wrapper "${wrapped}" -} - -_lsp_preflight_container_binary() { - local binary="$1" - # Use wrap_command to build ` command -v ` and exec it. - # Exit code != 0 means binary not available in the containerized context. - # - # CRITICAL: redirect stdin from /dev/null. The wrapped command uses - # `docker exec -i` which forwards host stdin into the container. Our - # stdin is the LSP client's pipe, and it may already contain the - # initialize request. Without /dev/null 2>&1; then - return 1 - fi - return 0 -} - -# shared/config.sh reads CONFIG_FILE_PREFIX and CONFIG_ENV_VAR_PREFIX. -# If config can't be loaded, fall back to the null stub silently. -if ! source "${SHARED_DIR}/config.sh" 2>/dev/null; then - _lsp_exec_null_stub "config.sh failed to source" -fi - -# Load config. If load_config fails (file missing/unreadable), fall back to stub. -if ! load_config "${PROJECT_ROOT}" 2>/dev/null; then - _lsp_exec_null_stub "no .lsp-${CONFIG_PREFIX}.json found" -fi - -# shared/environment.sh is reused unchanged. -source "${SHARED_DIR}/environment.sh" - -# Read LSP settings from the config file. -LSP_ENABLED=$(jq -r '.enabled // false' "${LINT_CONFIG_FILE}" 2>/dev/null || echo "false") -LSP_BINARY=$(jq -r ".binary // \"${LSP_DEFAULT_BINARY}\"" "${LINT_CONFIG_FILE}" 2>/dev/null || echo "${LSP_DEFAULT_BINARY}") -export LSP_ENABLED LSP_BINARY - -if [[ "${LSP_ENABLED}" != "true" ]]; then - _lsp_exec_null_stub "enabled=${LSP_ENABLED}" -fi - -# detect_environment hard-fails if `environment` is missing, which is what we want. -detect_environment "${PROJECT_ROOT}" - -lsp_run_or_null_stub() { - local binary_cmd="$1" - - case "${LINT_ENV}" in - native) - _lsp_exec_direct "${binary_cmd}" - ;; - docker|docker-compose|vagrant|ddev) - if ! _lsp_preflight_container_binary "${LSP_BINARY}"; then - _lsp_exec_null_stub "preflight failed: ${LSP_BINARY} not found in ${LINT_ENV} context" - fi - # docker-compose sets LINT_WORKDIR to a lazy sentinel; resolve it now - # so the proxy's URI rewriter receives the real container path. - if [[ "${LINT_ENV}" == "docker-compose" ]]; then - LINT_WORKDIR=$(_compose_resolve_workdir) || { - log "ERROR" "failed to resolve docker-compose workdir: ${LINT_WORKDIR}" - _lsp_exec_null_stub "failed to resolve docker-compose workdir" - } - fi - local wrapped - wrapped=$(wrap_command "${binary_cmd}") - _lsp_exec_proxy "${wrapped}" - ;; - *) - log "ERROR" "unsupported environment: ${LINT_ENV}" - _lsp_exec_null_stub "unsupported environment" - ;; - esac -} diff --git a/plugins/dev-tooling/shared/lsp_null.sh b/plugins/dev-tooling/shared/lsp_null.sh deleted file mode 100755 index 97c8a5b4..00000000 --- a/plugins/dev-tooling/shared/lsp_null.sh +++ /dev/null @@ -1,99 +0,0 @@ -#!/usr/bin/env bash -# Minimal LSP "null" server. -# -# Accepts an LSP `initialize` request and responds with empty server -# capabilities, then idles until the client sends `shutdown` + `exit`. -# Used to cleanly disable an LSP entry in .lsp.json without triggering -# Claude Code's crash-and-restart logic: Claude Code sees a valid, -# initialized language server that advertises zero capabilities, so it -# never issues further requests against it. -# -# Protocol: LSP over stdio with Content-Length framing (LSP spec §3). -# Dependencies: bash 4+, jq (already a dev-tooling prerequisite). -# No Python, Node, or other runtimes required. - -set -uo pipefail # no -e: read failures on client EOF are expected - -# Read a single LSP message from stdin. -# Writes the JSON body to stdout; returns non-zero on EOF or malformed headers. -read_message() { - local line content_length=0 raw - - # Parse headers until blank line (end-of-headers marker). - while IFS= read -r line; do - line="${line%$'\r'}" - [[ -z "${line}" ]] && break - if [[ "${line}" =~ ^Content-Length:[[:space:]]*([0-9]+)$ ]]; then - content_length="${BASH_REMATCH[1]}" - fi - done - - [[ "${content_length}" -eq 0 ]] && return 1 - - # Read exactly N bytes of body. We deliberately avoid `head -c` here: - # `head` uses buffered stdin reads and may over-consume past byte N on - # a pipe, dropping bytes that belong to the next LSP message. `dd bs=1` - # reads one byte at a time, guaranteeing no over-read. - # The trailing 'X' sentinel protects against command-substitution - # stripping a legitimate trailing newline at byte N. - raw=$(dd bs=1 count="${content_length}" 2>/dev/null; printf 'X') - printf '%s' "${raw%X}" -} - -# Write a single LSP message to stdout with Content-Length framing. -# Bash's builtin printf writes via write(2) with no libc buffering, -# so the frame is delivered immediately in a single syscall. -write_message() { - local body="$1" - printf 'Content-Length: %d\r\n\r\n%s' "${#body}" "${body}" -} - -main() { - local body method resp - - while body=$(read_message); do - [[ -z "${body}" ]] && continue - - method=$(printf '%s' "${body}" | jq -r '.method // ""') - - case "${method}" in - initialize) - # Echo back the request id with its original JSON type - # (number vs string) preserved by doing the transform in jq. - resp=$(printf '%s' "${body}" \ - | jq -c '{jsonrpc:"2.0",id:.id,result:{capabilities:{}}}') - write_message "${resp}" - ;; - shutdown) - resp=$(printf '%s' "${body}" \ - | jq -c '{jsonrpc:"2.0",id:.id,result:null}') - write_message "${resp}" - ;; - exit) - # Per LSP spec: after shutdown, the client sends `exit` - # and the server must terminate. - exit 0 - ;; - *) - # Notifications (no id field) are dropped silently. - # Requests (with id) MUST get a response or the client hangs. - # Claude Code sends textDocument/documentSymbol unconditionally - # regardless of advertised capabilities, so we cannot rely on - # capability negotiation alone. Reply with JSON-RPC - # MethodNotFound (-32601), preserving the request id type. - if [[ "$(printf '%s' "${body}" | jq 'has("id")')" == "true" ]]; then - resp=$(printf '%s' "${body}" | jq -c '{ - jsonrpc:"2.0", - id:.id, - error:{code:-32601,message:"Method not found"} - }') - write_message "${resp}" - fi - ;; - esac - done - - exit 0 -} - -main "$@" diff --git a/plugins/dev-tooling/shared/lsp_proxy.py b/plugins/dev-tooling/shared/lsp_proxy.py deleted file mode 100644 index a0fa7efb..00000000 --- a/plugins/dev-tooling/shared/lsp_proxy.py +++ /dev/null @@ -1,277 +0,0 @@ -#!/usr/bin/env python3 -"""LSP stdio proxy with URI rewriting for containerized language servers. - -Reads Content-Length-framed JSON-RPC messages from stdin, forwards them to a -spawned child process (typically `docker compose exec -T `), reads -replies from the child's stdout, and writes them back to stdout. On every -frame, `file://` URIs are string-substituted in both directions to translate -between host and container paths. - -Invoked from shared/lsp_bootstrap.sh for environments other than native. -Never invoked for `environment: native` — the dispatcher `exec`s directly -into the LSP binary in that case. - -Dependencies: Python 3.12+ stdlib only. No third-party packages. -""" - -from __future__ import annotations - -import asyncio -import contextlib -import sys -from typing import BinaryIO, Protocol - - -class FramingError(Exception): - """Raised when an LSP message frame is malformed beyond recovery.""" - - -class _AsyncByteWriter(Protocol): - """Minimal async writer interface the host→container pump needs.""" - - def write(self, data: bytes) -> None: ... - async def drain(self) -> None: ... - def close(self) -> None: ... - - -async def read_frame(reader: asyncio.StreamReader) -> bytes | None: - """Read exactly one LSP message frame from `reader`. - - Returns the body bytes on success, or None on clean EOF (before any - header byte has been read). Raises FramingError on malformed headers - or truncated bodies. - """ - # Read headers line by line until the blank line that ends them. - content_length: int | None = None - header_seen = False - while True: - try: - line = await reader.readuntil(b"\r\n") - except asyncio.IncompleteReadError as exc: - if not header_seen and not exc.partial: - return None # Clean EOF before any header byte - raise FramingError(f"Unexpected EOF in LSP headers (partial={exc.partial!r})") from exc - - header_seen = True - line = line[:-2] # Strip trailing \r\n - if line == b"": - break # End-of-headers blank line - if line.lower().startswith(b"content-length:"): - try: - content_length = int(line.split(b":", 1)[1].strip()) - except (IndexError, ValueError) as exc: - raise FramingError(f"Invalid Content-Length header: {line!r}") from exc - # Other headers (Content-Type, etc.) are ignored per LSP spec §3. - - if content_length is None: - raise FramingError("Frame headers missing required Content-Length") - - try: - body = await reader.readexactly(content_length) - except asyncio.IncompleteReadError as exc: - raise FramingError( - f"Truncated body: wanted {content_length} bytes, got {len(exc.partial)}" - ) from exc - return body - - -def write_frame(writer: BinaryIO, body: bytes) -> None: - """Write one LSP message frame to `writer`. - - The Content-Length header is computed from the byte length of `body`, - which is critical for correctness when the body contains multi-byte - UTF-8 characters. - """ - header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") - writer.write(header) - writer.write(body) - - -def rewrite_uris_host_to_container(body: bytes, host_root: str, container_root: str) -> bytes: - """Rewrite `file://` → `file://` in `body`. - - Byte-level substitution on the serialized JSON. Deliberately does not - parse JSON: the `file://` prefix is specific enough that false - positives are effectively impossible for any realistic `host_root`. - """ - needle = f"file://{host_root}".encode() - replacement = f"file://{container_root}".encode() - return body.replace(needle, replacement) - - -def rewrite_uris_container_to_host(body: bytes, host_root: str, container_root: str) -> bytes: - """Rewrite `file://` → `file://` in `body`.""" - needle = f"file://{container_root}".encode() - replacement = f"file://{host_root}".encode() - return body.replace(needle, replacement) - - -async def _pump_host_to_container( - src: asyncio.StreamReader, - dst: _AsyncByteWriter, - host_root: str, - container_root: str, -) -> None: - """Read frames from `src`, rewrite host→container URIs, write to `dst`.""" - try: - while True: - body = await read_frame(src) - if body is None: - break - rewritten = rewrite_uris_host_to_container(body, host_root, container_root) - header = f"Content-Length: {len(rewritten)}\r\n\r\n".encode("ascii") - dst.write(header) - dst.write(rewritten) - await dst.drain() - finally: - # Signal EOF to the child's stdin so it can exit cleanly after `exit`. - with contextlib.suppress(BrokenPipeError, ConnectionResetError): - dst.close() - - -async def _pump_container_to_host( - src: asyncio.StreamReader, - dst: BinaryIO, - host_root: str, - container_root: str, -) -> None: - """Read frames from `src`, rewrite container→host URIs, write to `dst`.""" - while True: - body = await read_frame(src) - if body is None: - break - rewritten = rewrite_uris_container_to_host(body, host_root, container_root) - header = f"Content-Length: {len(rewritten)}\r\n\r\n".encode("ascii") - dst.write(header) - dst.write(rewritten) - try: - dst.flush() - except (BrokenPipeError, ConnectionResetError): - break - - -async def run( - *, - host_root: str, - container_root: str, - wrapper: str, - stdin_reader: asyncio.StreamReader | None = None, - stdout_writer: BinaryIO | None = None, -) -> int: - """Top-level proxy coroutine. - - Spawns the wrapped child command, runs both pump loops concurrently, - and returns 0 on clean shutdown or non-zero on abnormal termination. - - `stdin_reader` and `stdout_writer` are injectable for tests. When None, - real stdin/stdout are used (via `connect_read_pipe` / direct `sys.stdout.buffer`). - """ - import shlex - - if stdin_reader is None: - stdin_reader = await _connect_stdin() - if stdout_writer is None: - stdout_writer = sys.stdout.buffer - - argv = shlex.split(wrapper) - try: - child = await asyncio.create_subprocess_exec( - *argv, - stdin=asyncio.subprocess.PIPE, - stdout=asyncio.subprocess.PIPE, - stderr=None, # Pass through to our stderr -> Claude Code's LSP log - ) - except FileNotFoundError as exc: - print(f"lsp_proxy: child spawn failed: {exc}", file=sys.stderr) - return 1 - - assert child.stdin is not None - assert child.stdout is not None - - # Wrap the subprocess transport writer for the host→container pump. - dst_writer = _StreamWriterWrapper(child.stdin) - - host_to_container = asyncio.create_task( - _pump_host_to_container(stdin_reader, dst_writer, host_root, container_root) - ) - container_to_host = asyncio.create_task( - _pump_container_to_host(child.stdout, stdout_writer, host_root, container_root) - ) - - results = await asyncio.gather(host_to_container, container_to_host, return_exceptions=True) - - # Wait for the child to actually exit. - try: - returncode = await asyncio.wait_for(child.wait(), timeout=5.0) - except TimeoutError: - child.kill() - returncode = await child.wait() - - # If either pump raised, surface it via stderr and exit non-zero. - for r in results: - if isinstance(r, Exception) and not isinstance(r, asyncio.CancelledError): - print(f"lsp_proxy: pump error: {r!r}", file=sys.stderr) - return 1 - - return 0 if returncode == 0 else returncode - - -async def _connect_stdin() -> asyncio.StreamReader: - """Wrap sys.stdin as an asyncio StreamReader.""" - loop = asyncio.get_event_loop() - reader = asyncio.StreamReader() - protocol = asyncio.StreamReaderProtocol(reader) - await loop.connect_read_pipe(lambda: protocol, sys.stdin) - return reader - - -class _StreamWriterWrapper: - """Adapts an asyncio.StreamWriter to the subset of the API the pump uses. - - `asyncio.subprocess.Process.stdin` is already a StreamWriter, so this - wrapper just forwards. It exists so tests can pass BytesIO-like objects - into pump helpers that expect `.write/.drain/.close`. - """ - - def __init__(self, inner: asyncio.StreamWriter) -> None: - self._inner = inner - - def write(self, data: bytes) -> None: - self._inner.write(data) - - async def drain(self) -> None: - await self._inner.drain() - - def close(self) -> None: - self._inner.close() - - -def _main() -> int: - import argparse - - parser = argparse.ArgumentParser( - description="LSP stdio proxy with URI rewriting for containerized LSPs" - ) - parser.add_argument("--host-root", required=True, help="Host project root path") - parser.add_argument("--container-root", required=True, help="Container project root path") - parser.add_argument( - "--wrapper", - required=True, - help="Full command string (split with shlex) to spawn the LSP child", - ) - args = parser.parse_args() - - try: - return asyncio.run( - run( - host_root=args.host_root, - container_root=args.container_root, - wrapper=args.wrapper, - ) - ) - except KeyboardInterrupt: - return 130 - - -if __name__ == "__main__": - sys.exit(_main()) diff --git a/plugins/plugin-setup/.claude-plugin/plugin.json b/plugins/plugin-setup/.claude-plugin/plugin.json index af8a2c5c..cf082b17 100644 --- a/plugins/plugin-setup/.claude-plugin/plugin.json +++ b/plugins/plugin-setup/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "plugin-setup", - "version": "1.0.3", + "version": "1.0.4", "description": "Interactive setup skills for plugins that ship a SETUP.md. Bundles one setting-up skill per supported plugin: dev-tooling, gh-tooling, and chunkhound-integration.", "author": { "name": "Shopware Labs" diff --git a/plugins/plugin-setup/CHANGELOG.md b/plugins/plugin-setup/CHANGELOG.md index 52d7e188..84956342 100644 --- a/plugins/plugin-setup/CHANGELOG.md +++ b/plugins/plugin-setup/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.4] - 2026-10-11 + +### Removed +- **The `dev-tooling-setting-up` skill no longer covers the phpactor LSP**, matching dev-tooling 5.0.0 removing the LSP. The skill no longer asks for `.lsp-php-tooling.json`, no longer checks `python3` or `phpactor` (the phar-sidecar install recipe is gone too), and no longer runs the LSP dispatcher dry-run or the other LSP validation steps. The containerized LSP lifecycle and `.phpactor.json` notes are gone from Post-Setup, and the skill description no longer mentions the LSP. + ## [1.0.3] - 2026-10-07 ### Changed diff --git a/plugins/plugin-setup/skills/chunkhound-integration-setting-up/SKILL.md b/plugins/plugin-setup/skills/chunkhound-integration-setting-up/SKILL.md index fca8f601..599878d6 100644 --- a/plugins/plugin-setup/skills/chunkhound-integration-setting-up/SKILL.md +++ b/plugins/plugin-setup/skills/chunkhound-integration-setting-up/SKILL.md @@ -1,6 +1,6 @@ --- name: chunkhound-integration-setting-up -version: 1.0.3 +version: 1.0.4 description: Use this skill when the user just installed the chunkhound-integration plugin and needs to configure it, asks how to set up semantic code search or ChunkHound — phrases like "help me set up chunkhound", "configure semantic search", "set up code research" — or when ChunkHound MCP tools fail with config or connection errors. Walks through prerequisite checks (chunkhound CLI, embedding provider — VoyageAI or OpenAI), creates .chunkhound.json with the chosen provider, runs the initial index, and validates the MCP server connection. model: sonnet allowed-tools: Bash, Read, Write, Glob, AskUserQuestion diff --git a/plugins/plugin-setup/skills/dev-tooling-setting-up/SKILL.md b/plugins/plugin-setup/skills/dev-tooling-setting-up/SKILL.md index a9c38e1f..7f4ec5c2 100644 --- a/plugins/plugin-setup/skills/dev-tooling-setting-up/SKILL.md +++ b/plugins/plugin-setup/skills/dev-tooling-setting-up/SKILL.md @@ -1,7 +1,7 @@ --- name: dev-tooling-setting-up -version: 1.0.3 -description: Use this skill when the user just installed the dev-tooling plugin and needs to configure it for a Shopware project, asks "help me set up dev-tooling" or for guidance on configuring the PHP and JavaScript tool chains, or when dev-tooling MCP tools fail with missing-config errors. Also use when reconfiguring the environment type (native, Docker, Docker Compose, Vagrant, DDEV) for the PHP and JS MCP servers. Checks prerequisites (jq), creates .mcp-php-tooling.json and .mcp-js-tooling.json, validates that the three MCP servers (PHP — PHPStan, ECS, Rector, PHPUnit; Administration JS; Storefront JS) connect, and walks through optional scope setup including the phpactor LSP. +version: 1.0.4 +description: Use this skill when the user just installed the dev-tooling plugin and needs to configure it for a Shopware project, asks "help me set up dev-tooling" or for guidance on configuring the PHP and JavaScript tool chains, or when dev-tooling MCP tools fail with missing-config errors. Also use when reconfiguring the environment type (native, Docker, Docker Compose, Vagrant, DDEV) for the PHP and JS MCP servers. Checks prerequisites (jq), creates .mcp-php-tooling.json and .mcp-js-tooling.json, validates that the three MCP servers (PHP — PHPStan, ECS, Rector, PHPUnit; Administration JS; Storefront JS) connect, and walks through optional plugin scope setup. model: sonnet allowed-tools: Bash, Read, Write, Glob, AskUserQuestion --- diff --git a/plugins/plugin-setup/skills/dev-tooling-setting-up/references/plugin-setup.md b/plugins/plugin-setup/skills/dev-tooling-setting-up/references/plugin-setup.md index c6a5ccbb..5f003db8 100644 --- a/plugins/plugin-setup/skills/dev-tooling-setting-up/references/plugin-setup.md +++ b/plugins/plugin-setup/skills/dev-tooling-setting-up/references/plugin-setup.md @@ -6,7 +6,6 @@ - [Configuration Files](#configuration-files) - [.mcp-php-tooling.json](#mcp-php-toolingjson) - [.mcp-js-tooling.json](#mcp-js-toolingjson) - - [.lsp-php-tooling.json](#lsp-php-toolingjson) - [Plugin Scope Setup](#plugin-scope-setup) - [Permission Groups](#permission-groups) - [Validation](#validation) @@ -21,62 +20,6 @@ - **Install**: https://jqlang.github.io/jq/download/ - **Required by**: All three MCP servers (php-tooling, js-admin-tooling, js-storefront-tooling) -### python3 -- **Check**: `python3 --version` -- **Install**: Usually pre-installed on macOS (Monterey+) and Linux. If missing: - - macOS: `brew install python@3.12` - - Debian/Ubuntu: `apt install python3` -- **Required by**: Only for PHP LSP support when using a containerized `environment` (docker, docker-compose, vagrant, ddev). Not required for native LSP or when LSP is disabled. Must be version 3.12 or newer. - -### phpactor (optional) -- **Check**: `phpactor --version` -- **Install (native/host)**: Download the phar release from https://github.com/phpactor/phpactor/releases/latest into `~/.local/bin/phpactor` (or anywhere on `$PATH`), then `chmod +x` it. Alternative: `composer global require phpactor/phpactor`. See https://phpactor.readthedocs.io/en/master/usage/standalone.html for details. Homebrew has no phpactor formula — do not attempt `brew install`. -- **Install (containerized)**: See the phar-sidecar recipe below. Do NOT use `composer require --dev phpactor/phpactor` in the project's `composer.json` for third-party repos like `shopware/shopware` — it pollutes the committed dependency list and pulls phpactor's transitive dependencies into the project's vendor tree. -- **Required by**: Optional PHP LSP integration (document symbols, hover, go-to-definition, references). The MCP servers work without it. For containerized environments the binary must be available inside the container; the host check can be skipped. - -#### Containerized install — phar sidecar (recommended for docker-compose) - -Use when the PHP service's base image does not ship phpactor (e.g. `ghcr.io/shopware/docker-dev`). A one-shot sidecar downloads the phar into a named volume; the PHP service mounts the volume read-only. No image rebuild, no `composer.json` pollution. The named volume persists across `docker compose down`; only `down -v` drops it and forces a re-download. - -Create `compose.override.yaml` next to the project's `compose.yaml`: - -```yaml -services: - phpactor-installer: - image: alpine/curl - restart: "no" - command: - - sh - - -c - - | - set -e - if [ ! -x /out/phpactor ]; then - curl -fsSL -o /out/phpactor \ - https://github.com/phpactor/phpactor/releases/latest/download/phpactor.phar - chmod +x /out/phpactor - fi - volumes: - - phpactor-bin:/out - - web: - depends_on: - phpactor-installer: - condition: service_completed_successfully - environment: - PHPACTOR_UNCONDITIONAL_TRUST: "1" - volumes: - - phpactor-bin:/opt/phpactor:ro - -volumes: - phpactor-bin: -``` - -Notes: -- Replace `web` with the actual service name if your stack differs. -- Binary path inside the container: `/opt/phpactor/phpactor`. Use this for the LSP binary-path question (question 10). -- `PHPACTOR_UNCONDITIONAL_TRUST=1` silences phpactor's per-project trust prompt. Without it, local `.phpactor.json` is ignored until you run `phpactor config:trust --trust` inside the container, and a fresh container re-prompts. The env var is the right default for containerized dev. -- Verify with `docker compose exec /opt/phpactor/phpactor --version` before running the LSP dispatcher check. - ## Configuration Files ### Scope of this setup @@ -212,69 +155,6 @@ Stored as `enforce_mcp_tools` in the config. } ``` -### .lsp-php-tooling.json - -**Required:** No — only create this file if you want PHP LSP support (phpactor). - -**Location:** Project root or any supported tool directory (`.claude/`, `.cursor/`, etc.) — same discovery rules as `.mcp-php-tooling.json`. Plugin subdirectories are not scanned. - -**Prerequisite binary:** `phpactor` must be installed where the LSP will run: -- Native: on your host — install the phar release (see the phpactor prerequisite section above) -- Containerized: inside the container — use the phar-sidecar recipe from the phpactor prerequisite section. For projects where adding phpactor to the committed dev dependencies is acceptable, `composer require --dev phpactor/phpactor` also works. - -#### Setup Questions - -1. **Do you want PHP LSP support enabled?** - - Yes → continue with this section - - No → skip this section entirely - -2. **Where should the LSP run?** - - `native` → runs on your host; file URIs pass through unchanged, no Python required - - `docker` / `docker-compose` / `ddev` / `vagrant` → runs inside the container; requires `python3` ≥ 3.12 on the host and `phpactor` inside the container - -3. **Docker container name** (only if environment = docker): Container name from `docker ps`, e.g. `shopware_app`. - -4. **Docker working directory** (only if environment = docker, optional): Default `/var/www/html`. - -5. **Compose service name** (only if environment = docker-compose): Which service in your `docker-compose.yml` runs phpactor? Default: `web` - -6. **Compose working directory** (only if environment = docker-compose, optional): Working directory inside the compose service. Leave blank to use the service's default `WORKDIR`. - -7. **Compose file** (only if environment = docker-compose, optional): Path to a specific compose file. Leave blank to auto-discover from the project root. - -8. **Vagrant working directory** (only if environment = vagrant, optional): Default `/vagrant`. - -9. **DDEV working directory** (only if environment = ddev, optional): Default `/var/www/html`. - -10. **Binary path (optional)** - - Leave blank to look up `phpactor` in `$PATH` (works for native installs and for containerized setups where phpactor is on the container's `$PATH`). - - Containerized + phar-sidecar install: `/opt/phpactor/phpactor`. This matches the mount target from the recipe in the phpactor prerequisite section. - - Or provide any other absolute path. - -#### Minimal Config - -Native: - -```json -{ - "environment": "native", - "enabled": true -} -``` - -Containerized (docker-compose, matches a typical Shopware setup): - -```json -{ - "environment": "docker-compose", - "docker-compose": { - "service": "web", - "workdir": "/var/www/html" - }, - "enabled": true -} -``` - ## Plugin Scope Setup Optional phase. Writes one or more scopes into `.mcp-php-tooling.json` and/or `.mcp-js-tooling.json` and optionally pins one as `default_scope` when the user develops against a Shopware plugin in `custom/plugins//`. @@ -376,9 +256,7 @@ Validation runs in two stages. Stage 1 checks config-file existence and shape be - `.mcp-php-tooling.json` exists at a valid discovery location, parses as JSON, and matches the schema. - `.mcp-js-tooling.json` (if created) exists, parses, matches schema. -- `.lsp-php-tooling.json` (if created) exists, parses, matches schema. - Every scope has a `cwd` pointing at an existing directory relative to the project root. -- Dry-run the LSP dispatcher as described under "PHP LSP" below (does not require MCP server restart). ### Stage 2 — Post-restart (live MCP dispatch) @@ -401,54 +279,8 @@ Run each applicable check below. Skip any whose config file was not created. If - **Pass**: ESLint output with results - **Fail**: Connection error or "command not found" error -#### PHP LSP (only if .lsp-php-tooling.json was created) - -Step 1 — binary reachability: -- Native: run `phpactor --version` on the host (or the absolute path configured in `.lsp-php-tooling.json`). -- Containerized: run the equivalent inside the container, e.g. `docker compose exec /opt/phpactor/phpactor --version` for the phar-sidecar recipe. -- **Pass**: prints a version line. -- **Fail**: binary missing or not at the configured path. Fix the install before the dispatcher check: re-check the sidecar volume mount or the container's `$PATH`; run `docker compose up -d` if the sidecar has not completed. - -Step 2 — dispatcher dry-run against the current project: - -```bash -LSP_DISPATCH_DRY_RUN=1 PROJECT_ROOT="$(pwd)" \ - bash "${CLAUDE_SKILL_DIR}/../../lsp-server-php/lsp.sh" -``` - -- **Pass**: output contains `target=direct-exec` (native) or `target=python-proxy` (containerized). The dispatcher resolved config, enabled flag, environment, and the phpactor preflight. -- **Fail**: output contains `target=null-stub reason=...`. Read the reason: - - `no .lsp-php-tooling.json found` — config missing or unreadable - - `enabled=false` — flip `enabled` to `true` - - `preflight failed: phpactor not found in context` — install phpactor inside the container (or on the host for native) - - `unsupported environment` — environment value is not one of `native|docker|docker-compose|vagrant|ddev` - - `failed to resolve docker-compose workdir` — the compose service can't be resolved; check `service`, `file`, and that the stack is up -- Note: this check validates dispatch only. Live LSP behavior (document symbols, hover, etc.) can only be exercised by opening a `.php` file in Claude Code after restart. The `Method not found from plugin:dev-tooling:phpactor` error at runtime means the null stub was selected — rerun the dry-run check to diagnose. - ## Post-Setup - Restart Claude Code after creating configuration files. The MCP servers are loaded at startup and will not pick up new config files until restart. - If you change a configuration file later, you also need to restart Claude Code. - After restart, the dev-tooling MCP tools will appear in your tool list. You can verify by asking Claude to run PHPStan on a file. - -### Containerized LSP lifecycle - -If you enabled an LSP with a containerized `environment` (docker, docker-compose, vagrant, ddev), **the container must be running before you open Claude Code**. Language servers start lazily — Claude Code spawns the LSP process the first time a matching file is opened in the session. If the container is down at that moment, the dispatcher's preflight check fails, falls back to the null stub, and the session continues without LSP support until you quit and restart Claude Code. - -Practical recipe: - -1. `docker compose up -d` (or `ddev start`, etc.) -2. `claude` (start Claude Code) -3. Open a file to trigger LSP lazy spawn - -If you ever see `Method not found from plugin:dev-tooling:phpactor` when you expected real LSP behavior, the most likely cause is the container wasn't up at startup. Restart Claude Code after starting the container. - -### Project-root `.phpactor.json` side effect - -phpactor may write a `.phpactor.json` file into the project root on first LSP use to persist its own session state. For a project you own, commit it or add it to the repo's `.gitignore` as you see fit. For third-party repos such as `shopware/shopware` where you do not want to touch the committed `.gitignore` or any shared git configuration, add the file to your local clone's exclude list: - -```bash -echo ".phpactor.json" >> .git/info/exclude -``` - -`.git/info/exclude` is per-clone, never committed, and uses the same syntax as `.gitignore`. `git status` will stop reporting the file without affecting any other developer's workflow. diff --git a/plugins/shopware-env/shared/config.sh b/plugins/shopware-env/shared/config.sh index e3550829..ed09871e 100644 --- a/plugins/shopware-env/shared/config.sh +++ b/plugins/shopware-env/shared/config.sh @@ -25,28 +25,10 @@ if [[ -z "${CONFIG_PREFIX:-}" ]]; then exit 1 fi -# Generate config file name and environment variable from prefix. -# -# MCP path (default, CONFIG_FILE_PREFIX unset): +# Generate config file name and environment variable from prefix: # php-tooling -> .mcp-php-tooling.json, MCP_PHP_TOOLING_CONFIG -# -# LSP path (caller sets CONFIG_FILE_PREFIX=".lsp-", CONFIG_ENV_VAR_PREFIX="LSP"): -# php-tooling -> .lsp-php-tooling.json, LSP_PHP_TOOLING_CONFIG -# -# The LSP path is explicit by design — we don't default CONFIG_FILE_PREFIX to ".mcp-" -# because a silent fallback would mask caller bugs. MCP's .mcp- literal stays hardcoded -# in the else branch below. -if [[ -n "${CONFIG_FILE_PREFIX:-}" ]]; then - if [[ -z "${CONFIG_ENV_VAR_PREFIX:-}" ]]; then - echo "ERROR: CONFIG_ENV_VAR_PREFIX required when CONFIG_FILE_PREFIX is set" >&2 - exit 1 - fi - CONFIG_FILE_NAME="${CONFIG_FILE_PREFIX}${CONFIG_PREFIX}.json" - CONFIG_ENV_VAR="${CONFIG_ENV_VAR_PREFIX}_${CONFIG_PREFIX^^}_CONFIG" -else - CONFIG_FILE_NAME=".mcp-${CONFIG_PREFIX}.json" - CONFIG_ENV_VAR="MCP_${CONFIG_PREFIX^^}_CONFIG" -fi +CONFIG_FILE_NAME=".mcp-${CONFIG_PREFIX}.json" +CONFIG_ENV_VAR="MCP_${CONFIG_PREFIX^^}_CONFIG" CONFIG_ENV_VAR="${CONFIG_ENV_VAR//-/_}" # Configuration locations relative to PROJECT_ROOT diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 1f027321..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,60 +0,0 @@ -[project] -name = "ai-coding-tools" -version = "0.1.0" -requires-python = ">=3.12" -dependencies = [] - -[dependency-groups] -test = [ - "pytest>=9.0", - "pytest-subprocess>=1.5", - "pytest-asyncio>=1.0", - "pyyaml>=6.0", -] -build = [ - "pyyaml>=6.0", - "skills-ref @ git+https://github.com/agentskills/agentskills.git#subdirectory=skills-ref", -] -lint = [ - "ruff>=0.8", - "mypy>=1.13", -] - -[tool.pytest.ini_options] -pythonpath = ["scripts"] -testpaths = ["plugin-tests/dev-tooling/lsp_proxy"] -asyncio_mode = "auto" - -[tool.ruff] -target-version = "py312" -line-length = 100 -extend-exclude = [ - "agent-skills-export", - "plugins/dev-tooling/.js-lsp-attic", -] - -[tool.ruff.lint] -select = [ - "E", # pycodestyle errors - "W", # pycodestyle warnings - "F", # pyflakes - "I", # isort - "B", # flake8-bugbear - "UP", # pyupgrade - "SIM", # flake8-simplify - "RUF", # ruff-specific -] - -[tool.mypy] -python_version = "3.12" -strict = true -files = [ - "plugins/dev-tooling/shared/lsp_proxy.py", - "plugin-tests/dev-tooling/lsp_proxy", -] - -[[tool.mypy.overrides]] -module = ["conftest", "test_framing", "test_uri_rewrite", "test_error_paths", "test_lifecycle"] -disallow_untyped_defs = false -disallow_incomplete_defs = false -disallow_untyped_decorators = false diff --git a/templates/README.md b/templates/README.md index 39b193bf..e85a87fe 100644 --- a/templates/README.md +++ b/templates/README.md @@ -39,4 +39,6 @@ These files live in `plugins/dev-tooling/shared/` but are owned by dev-tooling, - `scope.sh` — full scope resolution system (dev-tooling only) - `worktree.sh` — per-call project root resolution for a linked git worktree (dev-tooling only) - `mcp-js-tooling.schema.json` — JS config schema (dev-tooling only) -- `lsp_bootstrap.sh`, `lsp_null.sh`, `lsp_proxy.py` — LSP support (dev-tooling only) +- `launch.sh` — launch mode, bound or unbound (dev-tooling only) +- `server_run.sh` — shared server tail around `run_mcp_server()` (dev-tooling only) +- `js_paths.sh` — JS path-scoped runs through the local binary (dev-tooling only) diff --git a/templates/mcp-shared/config.sh b/templates/mcp-shared/config.sh index e3550829..ed09871e 100644 --- a/templates/mcp-shared/config.sh +++ b/templates/mcp-shared/config.sh @@ -25,28 +25,10 @@ if [[ -z "${CONFIG_PREFIX:-}" ]]; then exit 1 fi -# Generate config file name and environment variable from prefix. -# -# MCP path (default, CONFIG_FILE_PREFIX unset): +# Generate config file name and environment variable from prefix: # php-tooling -> .mcp-php-tooling.json, MCP_PHP_TOOLING_CONFIG -# -# LSP path (caller sets CONFIG_FILE_PREFIX=".lsp-", CONFIG_ENV_VAR_PREFIX="LSP"): -# php-tooling -> .lsp-php-tooling.json, LSP_PHP_TOOLING_CONFIG -# -# The LSP path is explicit by design — we don't default CONFIG_FILE_PREFIX to ".mcp-" -# because a silent fallback would mask caller bugs. MCP's .mcp- literal stays hardcoded -# in the else branch below. -if [[ -n "${CONFIG_FILE_PREFIX:-}" ]]; then - if [[ -z "${CONFIG_ENV_VAR_PREFIX:-}" ]]; then - echo "ERROR: CONFIG_ENV_VAR_PREFIX required when CONFIG_FILE_PREFIX is set" >&2 - exit 1 - fi - CONFIG_FILE_NAME="${CONFIG_FILE_PREFIX}${CONFIG_PREFIX}.json" - CONFIG_ENV_VAR="${CONFIG_ENV_VAR_PREFIX}_${CONFIG_PREFIX^^}_CONFIG" -else - CONFIG_FILE_NAME=".mcp-${CONFIG_PREFIX}.json" - CONFIG_ENV_VAR="MCP_${CONFIG_PREFIX^^}_CONFIG" -fi +CONFIG_FILE_NAME=".mcp-${CONFIG_PREFIX}.json" +CONFIG_ENV_VAR="MCP_${CONFIG_PREFIX^^}_CONFIG" CONFIG_ENV_VAR="${CONFIG_ENV_VAR//-/_}" # Configuration locations relative to PROJECT_ROOT diff --git a/templates/plugin-setup/SKILL.md b/templates/plugin-setup/SKILL.md index ab0279c1..9244fa04 100644 --- a/templates/plugin-setup/SKILL.md +++ b/templates/plugin-setup/SKILL.md @@ -3,9 +3,9 @@ name: setting-up version: 0.0.0 description: > Interactive setup for this plugin. Walks through the prerequisites, creates - the required configuration files, and validates that MCP tools and/or LSP are - correctly wired. Use when the plugin is first installed or when tools fail - with missing-config errors. + the required configuration files, and validates that MCP tools are correctly + wired. Use when the plugin is first installed or when tools fail with + missing-config errors. model: sonnet allowed-tools: Bash, Read, Write, Glob, AskUserQuestion --- diff --git a/uv.lock b/uv.lock deleted file mode 100644 index 217cd6d2..00000000 --- a/uv.lock +++ /dev/null @@ -1,428 +0,0 @@ -version = 1 -revision = 3 -requires-python = ">=3.12" -resolution-markers = [ - "python_full_version >= '3.15'", - "python_full_version < '3.15'", -] - -[[package]] -name = "ai-coding-tools" -version = "0.1.0" -source = { virtual = "." } - -[package.dev-dependencies] -build = [ - { name = "pyyaml" }, - { name = "skills-ref" }, -] -lint = [ - { name = "mypy" }, - { name = "ruff" }, -] -test = [ - { name = "pytest" }, - { name = "pytest-asyncio" }, - { name = "pytest-subprocess" }, - { name = "pyyaml" }, -] - -[package.metadata] - -[package.metadata.requires-dev] -build = [ - { name = "pyyaml", specifier = ">=6.0" }, - { name = "skills-ref", git = "https://github.com/agentskills/agentskills.git?subdirectory=skills-ref" }, -] -lint = [ - { name = "mypy", specifier = ">=1.13" }, - { name = "ruff", specifier = ">=0.8" }, -] -test = [ - { name = "pytest", specifier = ">=9.0" }, - { name = "pytest-asyncio", specifier = ">=1.0" }, - { name = "pytest-subprocess", specifier = ">=1.5" }, - { name = "pyyaml", specifier = ">=6.0" }, -] - -[[package]] -name = "ast-serialize" -version = "0.5.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6", size = 61157, upload-time = "2026-05-17T17:48:29.429Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a", size = 1183520, upload-time = "2026-05-17T17:47:30.831Z" }, - { url = "https://files.pythonhosted.org/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b", size = 1175779, upload-time = "2026-05-17T17:47:32.551Z" }, - { url = "https://files.pythonhosted.org/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1", size = 1233750, upload-time = "2026-05-17T17:47:34.731Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6", size = 1235942, upload-time = "2026-05-17T17:47:36.287Z" }, - { url = "https://files.pythonhosted.org/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2", size = 1442517, upload-time = "2026-05-17T17:47:38.17Z" }, - { url = "https://files.pythonhosted.org/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903", size = 1254081, upload-time = "2026-05-17T17:47:39.826Z" }, - { url = "https://files.pythonhosted.org/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261", size = 1259910, upload-time = "2026-05-17T17:47:41.369Z" }, - { url = "https://files.pythonhosted.org/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027", size = 1250678, upload-time = "2026-05-17T17:47:43.702Z" }, - { url = "https://files.pythonhosted.org/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937", size = 1301603, upload-time = "2026-05-17T17:47:46.256Z" }, - { url = "https://files.pythonhosted.org/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c", size = 1410332, upload-time = "2026-05-17T17:47:47.899Z" }, - { url = "https://files.pythonhosted.org/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b", size = 1509979, upload-time = "2026-05-17T17:47:50.942Z" }, - { url = "https://files.pythonhosted.org/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab", size = 1505002, upload-time = "2026-05-17T17:47:54.093Z" }, - { url = "https://files.pythonhosted.org/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3", size = 1456231, upload-time = "2026-05-17T17:47:56.311Z" }, - { url = "https://files.pythonhosted.org/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38", size = 1058668, upload-time = "2026-05-17T17:47:58.305Z" }, - { url = "https://files.pythonhosted.org/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c", size = 1101075, upload-time = "2026-05-17T17:48:00.35Z" }, - { url = "https://files.pythonhosted.org/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb", size = 1075347, upload-time = "2026-05-17T17:48:01.753Z" }, - { url = "https://files.pythonhosted.org/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101", size = 1191380, upload-time = "2026-05-17T17:48:03.738Z" }, - { url = "https://files.pythonhosted.org/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a", size = 1183879, upload-time = "2026-05-17T17:48:05.463Z" }, - { url = "https://files.pythonhosted.org/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211", size = 1244529, upload-time = "2026-05-17T17:48:07.008Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf", size = 1240560, upload-time = "2026-05-17T17:48:08.46Z" }, - { url = "https://files.pythonhosted.org/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9", size = 1451172, upload-time = "2026-05-17T17:48:09.922Z" }, - { url = "https://files.pythonhosted.org/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee", size = 1265072, upload-time = "2026-05-17T17:48:11.469Z" }, - { url = "https://files.pythonhosted.org/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809", size = 1270488, upload-time = "2026-05-17T17:48:13.575Z" }, - { url = "https://files.pythonhosted.org/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43", size = 1260702, upload-time = "2026-05-17T17:48:15.141Z" }, - { url = "https://files.pythonhosted.org/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934", size = 1311182, upload-time = "2026-05-17T17:48:16.779Z" }, - { url = "https://files.pythonhosted.org/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759", size = 1421410, upload-time = "2026-05-17T17:48:18.527Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887", size = 1516587, upload-time = "2026-05-17T17:48:20.133Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27", size = 1515171, upload-time = "2026-05-17T17:48:21.921Z" }, - { url = "https://files.pythonhosted.org/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d", size = 1464668, upload-time = "2026-05-17T17:48:23.544Z" }, - { url = "https://files.pythonhosted.org/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a", size = 1068311, upload-time = "2026-05-17T17:48:25.027Z" }, - { url = "https://files.pythonhosted.org/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590", size = 1108931, upload-time = "2026-05-17T17:48:26.591Z" }, - { url = "https://files.pythonhosted.org/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642", size = 1081181, upload-time = "2026-05-17T17:48:28.122Z" }, -] - -[[package]] -name = "click" -version = "8.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, -] - -[[package]] -name = "colorama" -version = "0.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, -] - -[[package]] -name = "iniconfig" -version = "2.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, -] - -[[package]] -name = "librt" -version = "0.11.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1", size = 200139, upload-time = "2026-05-10T18:17:25.138Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46", size = 144147, upload-time = "2026-05-10T18:15:53.227Z" }, - { url = "https://files.pythonhosted.org/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3", size = 143614, upload-time = "2026-05-10T18:15:54.657Z" }, - { url = "https://files.pythonhosted.org/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67", size = 485538, upload-time = "2026-05-10T18:15:56.117Z" }, - { url = "https://files.pythonhosted.org/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a", size = 479623, upload-time = "2026-05-10T18:15:57.544Z" }, - { url = "https://files.pythonhosted.org/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a", size = 513082, upload-time = "2026-05-10T18:15:58.805Z" }, - { url = "https://files.pythonhosted.org/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f", size = 508105, upload-time = "2026-05-10T18:16:00.2Z" }, - { url = "https://files.pythonhosted.org/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b", size = 522268, upload-time = "2026-05-10T18:16:01.708Z" }, - { url = "https://files.pythonhosted.org/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766", size = 527348, upload-time = "2026-05-10T18:16:03.496Z" }, - { url = "https://files.pythonhosted.org/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d", size = 516294, upload-time = "2026-05-10T18:16:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8", size = 553608, upload-time = "2026-05-10T18:16:06.839Z" }, - { url = "https://files.pythonhosted.org/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a", size = 101879, upload-time = "2026-05-10T18:16:08.103Z" }, - { url = "https://files.pythonhosted.org/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9", size = 119831, upload-time = "2026-05-10T18:16:09.174Z" }, - { url = "https://files.pythonhosted.org/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c", size = 103470, upload-time = "2026-05-10T18:16:10.369Z" }, - { url = "https://files.pythonhosted.org/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894", size = 144119, upload-time = "2026-05-10T18:16:11.771Z" }, - { url = "https://files.pythonhosted.org/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c", size = 143565, upload-time = "2026-05-10T18:16:13.334Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea", size = 485395, upload-time = "2026-05-10T18:16:14.729Z" }, - { url = "https://files.pythonhosted.org/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230", size = 479383, upload-time = "2026-05-10T18:16:16.321Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2", size = 513010, upload-time = "2026-05-10T18:16:17.647Z" }, - { url = "https://files.pythonhosted.org/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3", size = 508433, upload-time = "2026-05-10T18:16:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21", size = 522595, upload-time = "2026-05-10T18:16:20.642Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930", size = 527255, upload-time = "2026-05-10T18:16:22.352Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be", size = 516847, upload-time = "2026-05-10T18:16:23.627Z" }, - { url = "https://files.pythonhosted.org/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e", size = 553920, upload-time = "2026-05-10T18:16:25.025Z" }, - { url = "https://files.pythonhosted.org/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e", size = 101898, upload-time = "2026-05-10T18:16:26.649Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47", size = 119812, upload-time = "2026-05-10T18:16:27.859Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44", size = 103448, upload-time = "2026-05-10T18:16:29.066Z" }, - { url = "https://files.pythonhosted.org/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd", size = 143345, upload-time = "2026-05-10T18:16:30.674Z" }, - { url = "https://files.pythonhosted.org/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4", size = 143131, upload-time = "2026-05-10T18:16:32.037Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8", size = 477024, upload-time = "2026-05-10T18:16:33.493Z" }, - { url = "https://files.pythonhosted.org/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b", size = 474221, upload-time = "2026-05-10T18:16:34.864Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175", size = 505174, upload-time = "2026-05-10T18:16:36.705Z" }, - { url = "https://files.pythonhosted.org/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03", size = 497216, upload-time = "2026-05-10T18:16:38.418Z" }, - { url = "https://files.pythonhosted.org/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c", size = 513921, upload-time = "2026-05-10T18:16:39.848Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3", size = 520850, upload-time = "2026-05-10T18:16:41.471Z" }, - { url = "https://files.pythonhosted.org/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96", size = 504237, upload-time = "2026-05-10T18:16:43.15Z" }, - { url = "https://files.pythonhosted.org/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe", size = 546261, upload-time = "2026-05-10T18:16:44.408Z" }, - { url = "https://files.pythonhosted.org/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f", size = 96965, upload-time = "2026-05-10T18:16:46.039Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7", size = 115151, upload-time = "2026-05-10T18:16:47.133Z" }, - { url = "https://files.pythonhosted.org/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1", size = 98850, upload-time = "2026-05-10T18:16:48.597Z" }, - { url = "https://files.pythonhosted.org/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72", size = 151138, upload-time = "2026-05-10T18:16:49.839Z" }, - { url = "https://files.pythonhosted.org/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa", size = 151976, upload-time = "2026-05-10T18:16:51.062Z" }, - { url = "https://files.pythonhosted.org/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548", size = 557927, upload-time = "2026-05-10T18:16:52.632Z" }, - { url = "https://files.pythonhosted.org/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2", size = 539698, upload-time = "2026-05-10T18:16:53.934Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f", size = 577162, upload-time = "2026-05-10T18:16:55.589Z" }, - { url = "https://files.pythonhosted.org/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51", size = 566494, upload-time = "2026-05-10T18:16:56.975Z" }, - { url = "https://files.pythonhosted.org/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2", size = 596858, upload-time = "2026-05-10T18:16:58.374Z" }, - { url = "https://files.pythonhosted.org/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085", size = 590318, upload-time = "2026-05-10T18:16:59.676Z" }, - { url = "https://files.pythonhosted.org/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3", size = 575115, upload-time = "2026-05-10T18:17:01.007Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd", size = 617918, upload-time = "2026-05-10T18:17:02.682Z" }, - { url = "https://files.pythonhosted.org/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8", size = 103562, upload-time = "2026-05-10T18:17:03.99Z" }, - { url = "https://files.pythonhosted.org/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c", size = 124327, upload-time = "2026-05-10T18:17:05.465Z" }, - { url = "https://files.pythonhosted.org/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253", size = 102572, upload-time = "2026-05-10T18:17:06.809Z" }, -] - -[[package]] -name = "mypy" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ast-serialize" }, - { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, - { name = "mypy-extensions" }, - { name = "pathspec" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633", size = 3898359, upload-time = "2026-05-11T18:37:36.237Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af", size = 14874381, upload-time = "2026-05-11T18:37:31.784Z" }, - { url = "https://files.pythonhosted.org/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6", size = 13665501, upload-time = "2026-05-11T18:34:23.063Z" }, - { url = "https://files.pythonhosted.org/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211", size = 14045750, upload-time = "2026-05-11T18:31:48.151Z" }, - { url = "https://files.pythonhosted.org/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b", size = 15061630, upload-time = "2026-05-11T18:37:06.898Z" }, - { url = "https://files.pythonhosted.org/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22", size = 15288831, upload-time = "2026-05-11T18:31:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b", size = 11135228, upload-time = "2026-05-11T18:34:31.23Z" }, - { url = "https://files.pythonhosted.org/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8", size = 10040684, upload-time = "2026-05-11T18:36:48.199Z" }, - { url = "https://files.pythonhosted.org/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5", size = 14852174, upload-time = "2026-05-11T18:31:38.929Z" }, - { url = "https://files.pythonhosted.org/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e", size = 13651542, upload-time = "2026-05-11T18:36:04.636Z" }, - { url = "https://files.pythonhosted.org/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e", size = 14033929, upload-time = "2026-05-11T18:35:55.742Z" }, - { url = "https://files.pythonhosted.org/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285", size = 15039200, upload-time = "2026-05-11T18:33:10.281Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5", size = 15272690, upload-time = "2026-05-11T18:32:07.205Z" }, - { url = "https://files.pythonhosted.org/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65", size = 11147435, upload-time = "2026-05-11T18:33:56.477Z" }, - { url = "https://files.pythonhosted.org/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d", size = 10035052, upload-time = "2026-05-11T18:32:30.049Z" }, - { url = "https://files.pythonhosted.org/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2", size = 14848422, upload-time = "2026-05-11T18:35:45.984Z" }, - { url = "https://files.pythonhosted.org/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f", size = 13677374, upload-time = "2026-05-11T18:36:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4", size = 14055743, upload-time = "2026-05-11T18:35:18.361Z" }, - { url = "https://files.pythonhosted.org/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef", size = 15020937, upload-time = "2026-05-11T18:34:59.618Z" }, - { url = "https://files.pythonhosted.org/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135", size = 15253371, upload-time = "2026-05-11T18:36:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21", size = 11326429, upload-time = "2026-05-11T18:34:13.526Z" }, - { url = "https://files.pythonhosted.org/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57", size = 10218799, upload-time = "2026-05-11T18:32:23.491Z" }, - { url = "https://files.pythonhosted.org/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e", size = 15923458, upload-time = "2026-05-11T18:35:28.64Z" }, - { url = "https://files.pythonhosted.org/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780", size = 14757697, upload-time = "2026-05-11T18:36:14.208Z" }, - { url = "https://files.pythonhosted.org/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd", size = 15405638, upload-time = "2026-05-11T18:33:48.249Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08", size = 16215852, upload-time = "2026-05-11T18:32:50.296Z" }, - { url = "https://files.pythonhosted.org/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081", size = 16452695, upload-time = "2026-05-11T18:33:38.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7", size = 12866622, upload-time = "2026-05-11T18:34:39.945Z" }, - { url = "https://files.pythonhosted.org/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6", size = 10610798, upload-time = "2026-05-11T18:36:31.444Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289", size = 2693302, upload-time = "2026-05-11T18:31:29.246Z" }, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - -[[package]] -name = "packaging" -version = "26.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, -] - -[[package]] -name = "pathspec" -version = "1.1.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, -] - -[[package]] -name = "pygments" -version = "2.20.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, -] - -[[package]] -name = "pytest" -version = "9.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "iniconfig" }, - { name = "packaging" }, - { name = "pluggy" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, -] - -[[package]] -name = "pytest-asyncio" -version = "1.4.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/43/7c/d36d04db312ecf4298932ef77e6e4a9e8ad017906e24e34f0b0c361a2473/pytest_asyncio-1.4.0.tar.gz", hash = "sha256:c6c0d2259945122819f171a32ecea2c349ead889ee28176caaf492143424be42", size = 58514, upload-time = "2026-05-26T09:56:04.083Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/e2/08a497ef684b88559c9cc5f4ad53a37e7b99e727094a86d6ea32536d5d3c/pytest_asyncio-1.4.0-py3-none-any.whl", hash = "sha256:933ca923a23075a87fb7070c0ec272a6848489824d887c85c812670932835aa1", size = 16930, upload-time = "2026-05-26T09:56:02.576Z" }, -] - -[[package]] -name = "pytest-subprocess" -version = "1.6.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/76/7a/0d5855132e11de2a96da26e596560757ebbbf8190cfe36cbf85d7423f384/pytest_subprocess-1.6.0.tar.gz", hash = "sha256:b2d746eb1b768a6f9087e5c7c91f87fb9d40c7fdc777550dc00397af428a0654", size = 47910, upload-time = "2026-05-10T08:22:54.207Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/4f/ebe38bf128380f6a8a9b0fbbbe24cbf83915bb2f934717be65cadf55b6fa/pytest_subprocess-1.6.0-py3-none-any.whl", hash = "sha256:00037100f30429c8546adc81f357fddb5213eb036fe3bfb47b7b6befc965e5b2", size = 23803, upload-time = "2026-05-10T08:22:52.52Z" }, -] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "six" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - -[[package]] -name = "ruff" -version = "0.15.14" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dc/8a/8bce2894573e9dae6ff4d77fe34ad727d79b9e6238ad288c5638990d90f6/ruff-0.15.14.tar.gz", hash = "sha256:48e866b165be4a9bdbf310f7d3c9a07edef2fe8cd63ffeb4e00bb590506ebf9f", size = 4700910, upload-time = "2026-05-21T14:34:55.177Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b9/c8/74a92c6ff9fcfb4f1f947126d3ebee8389276e161ecc85de5bda7cda51bd/ruff-0.15.14-py3-none-linux_armv6l.whl", hash = "sha256:8dd2db9416e487c8d4b01fa7056bb02c4d05969d4f8d17a08c229c2f4ff3c108", size = 10739177, upload-time = "2026-05-21T14:34:37.332Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/254a35c20acc38a7223c9d2d594af12e794432464f2cdeb52af1dc4a892d/ruff-0.15.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:be4ff55af755bd71a00ab3dc6bd7ffc467bd76e0df6881e286c2e3d23e8fb43b", size = 11144969, upload-time = "2026-05-21T14:34:43.978Z" }, - { url = "https://files.pythonhosted.org/packages/56/9e/d13e40f83b8d0a94430e6778ce1d94a43b38cf2efe63278bdd2b4c65abbf/ruff-0.15.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:48d5909d7d06276ce7dde6d32bfa4b0d4cb2651145cd8ee4b440722cbc77832f", size = 10478207, upload-time = "2026-05-21T14:34:48.378Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f1/b15a7839fa4f332f8acec78e20564f26bb2d866e3d21710b877fd0263000/ruff-0.15.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca8cbfa94c4f90984a67561978602746d4cd27103568f745fa90eee3f0d4107d", size = 10818459, upload-time = "2026-05-21T14:34:22.318Z" }, - { url = "https://files.pythonhosted.org/packages/45/33/53d651177f84f94b400a0e27f8824eeada3dddc9d5ee8aeb048f4352a520/ruff-0.15.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a6bbc0333f1ab053423bcbf6226477d266ca7cec7738c4c8e3f55647803f3c4", size = 10541800, upload-time = "2026-05-21T14:34:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/b8/a6/868f87e0bf9786ed24b5d0d0ad8676b8a94fd1912f42cddf9cfc7857818a/ruff-0.15.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a24a4f7605d7003a6674d4387651effd939dead3fddd0f36561eb77a9a2e542", size = 11342149, upload-time = "2026-05-21T14:34:46.365Z" }, - { url = "https://files.pythonhosted.org/packages/a7/8b/38cd5c19faffdcc05a408d2b78edccc69492ab9720eadb49ea15ef80d768/ruff-0.15.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:049b5326e53ed80978f2fc041a280603f69dd6b0c95464342a2bb4572d9d9e2f", size = 12212563, upload-time = "2026-05-21T14:34:28.579Z" }, - { url = "https://files.pythonhosted.org/packages/3e/4d/a3c5b874a556d5731e3e657aaf04311bb76f0a5c3ec220ed43051be6b64b/ruff-0.15.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4ed42e6696c8dfa5f06728e6441993901f548eb92d73bc472cb5a38d1395fbf", size = 11493299, upload-time = "2026-05-21T14:34:41.836Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c0/56472c251d09858a53e51efbd485b09e1995d8731668b76d52e5dd6ee0f1/ruff-0.15.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:715c543cf450c4888251f91c52f1942a800541d9bddd7ac060aa4e6b77ae7cba", size = 11455931, upload-time = "2026-05-21T14:34:57.276Z" }, - { url = "https://files.pythonhosted.org/packages/2c/4a/e2e7b4d8dbf233d4eace59c75bc3435fa6d8bd3bae82d351d4e4300c0fd1/ruff-0.15.14-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:72ebab6013ec887d439d8b7593737a0a4ffb06d45d209d4e4bf2e92813082d3f", size = 11400794, upload-time = "2026-05-21T14:34:39.773Z" }, - { url = "https://files.pythonhosted.org/packages/97/c7/83c0539fe34c3e09136204d1e75d6052492364e0b3cb05e9465423f567d7/ruff-0.15.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:49072d36abdbe97a8dd7f480afe9c675699c0c495d4c84076e2c1203c4550581", size = 10804759, upload-time = "2026-05-21T14:34:31.045Z" }, - { url = "https://files.pythonhosted.org/packages/86/a6/18f2bfc095a2ab4a78745644e428205532ce6653a5d0fa8501572891534d/ruff-0.15.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:958522aee105068640c2c2ceae08f413ae44d922f52a1374ac13d6a96032fc93", size = 10539517, upload-time = "2026-05-21T14:34:53.064Z" }, - { url = "https://files.pythonhosted.org/packages/54/3a/5a8b3b69c654d4e4bf1d246ac5b49cbcdac6eaab6905925f8915f31e3b80/ruff-0.15.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f3707da619a143a2e8830e2abab8224478d69ace2d28cb6c20543ae97c36bf61", size = 11065169, upload-time = "2026-05-21T14:34:24.484Z" }, - { url = "https://files.pythonhosted.org/packages/ed/c5/8864e4e7925b836ea354b31d57641ec03830564e281a8b6f061f8c3e0ec1/ruff-0.15.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:bb01d645694e3ec0102105d07ef2d53703970407d59c04e59d3ba0b7a1d53553", size = 11560214, upload-time = "2026-05-21T14:34:50.975Z" }, - { url = "https://files.pythonhosted.org/packages/36/38/012bf76752e1f89ed50b77b99532d90f3a3e287bc7918e1fc0948ac866ac/ruff-0.15.14-py3-none-win32.whl", hash = "sha256:6d0c1ad2a0ab718d39b6d8fd2217981ce4d625cd96a720095f798fb47d8b13e6", size = 10805548, upload-time = "2026-05-21T14:34:33.453Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b7/4ea2c170f10ad760fff2a5250beb18897719dc8b52b53a24cddbb9dd3f19/ruff-0.15.14-py3-none-win_amd64.whl", hash = "sha256:802342981e056db3851a7836e5b070f8f15f67d4a685ae2a6160939d364b2902", size = 11939523, upload-time = "2026-05-21T14:34:18.077Z" }, - { url = "https://files.pythonhosted.org/packages/62/d5/bc97ff895ec35cf3925d4bd60f3b39d822f377a446906ec9bcc87405e59b/ruff-0.15.14-py3-none-win_arm64.whl", hash = "sha256:ff47b90a9ef6a40c9e2f3b479c1fb78531adf055b94c1eba0a7ba04b31951826", size = 11208607, upload-time = "2026-05-21T14:34:26.525Z" }, -] - -[[package]] -name = "six" -version = "1.17.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, -] - -[[package]] -name = "skills-ref" -version = "0.1.0" -source = { git = "https://github.com/agentskills/agentskills.git?subdirectory=skills-ref#5d4c1fda3f786fff826c7f56b6cb3341e7f3a911" } -dependencies = [ - { name = "click" }, - { name = "strictyaml" }, -] - -[[package]] -name = "strictyaml" -version = "1.7.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "python-dateutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/08/efd28d49162ce89c2ad61a88bd80e11fb77bc9f6c145402589112d38f8af/strictyaml-1.7.3.tar.gz", hash = "sha256:22f854a5fcab42b5ddba8030a0e4be51ca89af0267961c8d6cfa86395586c407", size = 115206, upload-time = "2023-03-10T12:50:27.062Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/7c/a81ef5ef10978dd073a854e0fa93b5d8021d0594b639cc8f6453c3c78a1d/strictyaml-1.7.3-py3-none-any.whl", hash = "sha256:fb5c8a4edb43bebb765959e420f9b3978d7f1af88c80606c03fb420888f5d1c7", size = 123917, upload-time = "2023-03-10T12:50:17.242Z" }, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] From 91bf9437dc9fa11c5bdbaa9e97e5f522573ccec0 Mon Sep 17 00:00:00 2001 From: Martin Bens Date: Sun, 11 Oct 2026 23:54:22 +0200 Subject: [PATCH 2/2] chore(marketplace): undo the dev-tooling and plugin-setup version bumps The release bump for the LSP removal ships in a separate change, so dev-tooling returns to 4.1.1 and plugin-setup to 1.0.3, and their `CHANGELOG.md` entries for 5.0.0 and 1.0.4 are removed. Co-Authored-By: Claude --- plugins/dev-tooling/.claude-plugin/plugin.json | 2 +- plugins/dev-tooling/.codex-plugin/plugin.json | 2 +- plugins/dev-tooling/CHANGELOG.md | 7 ------- plugins/plugin-setup/.claude-plugin/plugin.json | 2 +- plugins/plugin-setup/CHANGELOG.md | 5 ----- .../skills/chunkhound-integration-setting-up/SKILL.md | 2 +- .../plugin-setup/skills/dev-tooling-setting-up/SKILL.md | 2 +- 7 files changed, 5 insertions(+), 17 deletions(-) diff --git a/plugins/dev-tooling/.claude-plugin/plugin.json b/plugins/dev-tooling/.claude-plugin/plugin.json index 91ce8d93..c6a737e8 100644 --- a/plugins/dev-tooling/.claude-plugin/plugin.json +++ b/plugins/dev-tooling/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dev-tooling", - "version": "5.0.0", + "version": "4.1.1", "description": "Three MCP servers for PHP and JavaScript operations. PHP (php-tooling): PHPStan, ECS, Rector, PHPUnit, PHPUnit coverage gap analysis, Symfony Console. Administration (js-admin-tooling): ESLint, Stylelint, Prettier, Jest, TypeScript, Vite builds. Storefront (js-storefront-tooling): ESLint, Stylelint, Jest, Webpack builds. Includes SessionStart hook that injects MCP tool directives and PreToolUse hooks that enforce MCP tool usage by blocking bash commands. Supports native, Docker, Docker Compose, Vagrant and DDEV environments.", "author": { "name": "Shopware Labs" diff --git a/plugins/dev-tooling/.codex-plugin/plugin.json b/plugins/dev-tooling/.codex-plugin/plugin.json index 3f6bdb4c..c263b55e 100644 --- a/plugins/dev-tooling/.codex-plugin/plugin.json +++ b/plugins/dev-tooling/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "dev-tooling", - "version": "5.0.0", + "version": "4.1.1", "description": "Three MCP servers for PHP and JavaScript operations. PHP (php-tooling): PHPStan, ECS, Rector, PHPUnit, PHPUnit coverage gap analysis, Symfony Console. Administration (js-admin-tooling): ESLint, Stylelint, Prettier, Jest, TypeScript, Vite builds. Storefront (js-storefront-tooling): ESLint, Stylelint, Jest, Webpack builds. Includes SessionStart hook that injects MCP tool directives and PreToolUse hooks that enforce MCP tool usage by blocking bash commands. Supports native, Docker, Docker Compose, Vagrant and DDEV environments.", "author": { "name": "Shopware Labs" diff --git a/plugins/dev-tooling/CHANGELOG.md b/plugins/dev-tooling/CHANGELOG.md index 12785362..3e00b247 100644 --- a/plugins/dev-tooling/CHANGELOG.md +++ b/plugins/dev-tooling/CHANGELOG.md @@ -5,13 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [5.0.0] - 2026-10-11 - -### Removed -- **The optional phpactor PHP language server is gone.** The plugin no longer registers `lsp-server-php` (`.lsp.json`), so Claude Code gets no document symbols, hover, go-to-definition or references for PHP from it. PHPStan through the `php-tooling` MCP server stays. The LSP launcher scripts, the Python URI-rewriting proxy and `docs/lsp.md` are deleted. -- **The LSP SessionStart directive is gone.** `hooks/scripts/lsp-directives.sh` and its two prompts are deleted and `hooks/hooks.json` runs `session-start.sh` alone. The plugin ignores `.lsp-php-tooling.json` and `LSP_PHP_TOOLING_CONFIG`; delete the file if you created one. -- **`python3` 3.12 and `phpactor` are no longer prerequisites.** Both were needed only for the LSP. The `dev-tooling` setup skill drops its `.lsp-php-tooling.json` questions, its phpactor install recipe and its LSP validation steps. - ## [4.1.1] - 2026-10-11 ### Changed diff --git a/plugins/plugin-setup/.claude-plugin/plugin.json b/plugins/plugin-setup/.claude-plugin/plugin.json index cf082b17..af8a2c5c 100644 --- a/plugins/plugin-setup/.claude-plugin/plugin.json +++ b/plugins/plugin-setup/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "plugin-setup", - "version": "1.0.4", + "version": "1.0.3", "description": "Interactive setup skills for plugins that ship a SETUP.md. Bundles one setting-up skill per supported plugin: dev-tooling, gh-tooling, and chunkhound-integration.", "author": { "name": "Shopware Labs" diff --git a/plugins/plugin-setup/CHANGELOG.md b/plugins/plugin-setup/CHANGELOG.md index 84956342..52d7e188 100644 --- a/plugins/plugin-setup/CHANGELOG.md +++ b/plugins/plugin-setup/CHANGELOG.md @@ -5,11 +5,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.0.4] - 2026-10-11 - -### Removed -- **The `dev-tooling-setting-up` skill no longer covers the phpactor LSP**, matching dev-tooling 5.0.0 removing the LSP. The skill no longer asks for `.lsp-php-tooling.json`, no longer checks `python3` or `phpactor` (the phar-sidecar install recipe is gone too), and no longer runs the LSP dispatcher dry-run or the other LSP validation steps. The containerized LSP lifecycle and `.phpactor.json` notes are gone from Post-Setup, and the skill description no longer mentions the LSP. - ## [1.0.3] - 2026-10-07 ### Changed diff --git a/plugins/plugin-setup/skills/chunkhound-integration-setting-up/SKILL.md b/plugins/plugin-setup/skills/chunkhound-integration-setting-up/SKILL.md index 599878d6..fca8f601 100644 --- a/plugins/plugin-setup/skills/chunkhound-integration-setting-up/SKILL.md +++ b/plugins/plugin-setup/skills/chunkhound-integration-setting-up/SKILL.md @@ -1,6 +1,6 @@ --- name: chunkhound-integration-setting-up -version: 1.0.4 +version: 1.0.3 description: Use this skill when the user just installed the chunkhound-integration plugin and needs to configure it, asks how to set up semantic code search or ChunkHound — phrases like "help me set up chunkhound", "configure semantic search", "set up code research" — or when ChunkHound MCP tools fail with config or connection errors. Walks through prerequisite checks (chunkhound CLI, embedding provider — VoyageAI or OpenAI), creates .chunkhound.json with the chosen provider, runs the initial index, and validates the MCP server connection. model: sonnet allowed-tools: Bash, Read, Write, Glob, AskUserQuestion diff --git a/plugins/plugin-setup/skills/dev-tooling-setting-up/SKILL.md b/plugins/plugin-setup/skills/dev-tooling-setting-up/SKILL.md index 7f4ec5c2..2dcc27e3 100644 --- a/plugins/plugin-setup/skills/dev-tooling-setting-up/SKILL.md +++ b/plugins/plugin-setup/skills/dev-tooling-setting-up/SKILL.md @@ -1,6 +1,6 @@ --- name: dev-tooling-setting-up -version: 1.0.4 +version: 1.0.3 description: Use this skill when the user just installed the dev-tooling plugin and needs to configure it for a Shopware project, asks "help me set up dev-tooling" or for guidance on configuring the PHP and JavaScript tool chains, or when dev-tooling MCP tools fail with missing-config errors. Also use when reconfiguring the environment type (native, Docker, Docker Compose, Vagrant, DDEV) for the PHP and JS MCP servers. Checks prerequisites (jq), creates .mcp-php-tooling.json and .mcp-js-tooling.json, validates that the three MCP servers (PHP — PHPStan, ECS, Rector, PHPUnit; Administration JS; Storefront JS) connect, and walks through optional plugin scope setup. model: sonnet allowed-tools: Bash, Read, Write, Glob, AskUserQuestion