Skip to content

Multi-host installer: Cursor, Windsurf, OpenCode, Claude#254

Merged
peteski22 merged 40 commits intomainfrom
feature/multi-host-installer
Apr 9, 2026
Merged

Multi-host installer: Cursor, Windsurf, OpenCode, Claude#254
peteski22 merged 40 commits intomainfrom
feature/multi-host-installer

Conversation

@peteski22
Copy link
Copy Markdown
Collaborator

@peteski22 peteski22 commented Apr 8, 2026

Summary

Replaces scripts/install-opencode.sh with a stdlib-only Python installer at scripts/install/ that supports four hosts: OpenCode, Cursor, Windsurf, and Claude Code (via the marketplace). Drops jq as a prereq, generalises the merge-not-replace MCP config behaviour into a primitive contract, and adds first-class Windows support.

Closes #154.
Picks up the Cursor + hook-helper direction from #166 (co-authored).

Key changes

Installer core (scripts/install/)

  • New uv project: stdlib-only at runtime, primitive library (common.py) for JSON merge, hook entries, Markdown blocks, manifest-tracked file copies, symlinks, write-if-missing, remove-owned-file. Per-host adapters under cq_install/hosts/.
  • python -m cq_install (install|uninstall) --target <host>... [--global | --project <path>] [--host-isolated-skills] [--dry-run]. Repeatable --target runs with shared-skills dedup via RunState.ensure_shared_skills.
  • --host-isolated-skills is validated against each host's supports_host_isolated flag; unsupported combinations are rejected with a clear error.
  • Config write hardening: _load_json wraps JSONDecodeError with the file path, upsert_json_entry raises ValueError on non-dict leaf values, upsert_hook_entry validates top-level shape (object, hooks object, entries list). Claude subprocess failures now surface returncode + stderr in the raised RuntimeError.

Binary fetch at install time

  • plugins/cq/scripts/cq_binary.py: new stdlib-only module containing the binary download, version check, and cache logic previously inlined in bootstrap.py. Single source of truth consumed by both the plugin bootstrap (for Claude) and the installer (for all other hosts).
  • cq_install.binary.ensure_cq_binary: dynamically loads cq_binary.py from the plugin source tree via importlib.util and calls its ensure_binary. Runs once per installer invocation via RunState.ensure_cq_binary so multi-target installs (install-all) fetch the binary exactly once.
  • Non-Claude MCP configs invoke the cq binary directly (runtime_root/bin/cq mcp) instead of going through python bootstrap.py, fixing MCP startup timeouts on Windows caused by Python stdio buffering.
  • plugins/cq/scripts/bootstrap.py is now a ~37-line launcher that imports cq_binary and execvps into cq mcp. Claude's plugin.json still points at it; the launch path is unchanged from Claude's perspective.

Runtime and metadata

  • Shared per-user runtime path at $XDG_DATA_HOME/cq/runtime/ (Windows: %LOCALAPPDATA%/cq/runtime/). Binary cache at runtime/bin/.
  • plugins/cq/scripts/bootstrap.json stores the required CLI version (cli_version) independently of .claude-plugin/plugin.json, so non-Claude hosts do not depend on the Claude plugin manifest.
  • Non-Claude runtime bundles reduced to only per-host assets: Windsurf and OpenCode no longer copy bootstrap files into the runtime; Cursor's bundle contains only cq_cursor_hook.py.
  • Path.home()-relative target dirs work cross-platform; OPENCODE_CONFIG_DIR env var honoured the same way OpenCode itself reads it.

Cursor host

  • Writes mcp.json, rules/cq.mdc (write-if-missing, never overwritten), and four lifecycle hooks (sessionStart, postToolUse, postToolUseFailure, stop) with platform-aware shell escaping (shlex.join POSIX, subprocess.list2cmdline Windows).
  • New stdlib-only Cursor hook helper at plugins/cq/hooks/cursor/cq_cursor_hook.py.

Build and CI

  • Makefile targets install-cursor, install-windsurf, install-claude, install-all plus matching uninstall-*. install-windsurf PROJECT=... prints a note and falls back to global since Windsurf has no per-project MCP config.
  • PowerShell wrapper at scripts/install.ps1 for Windows users (no make required).
  • Pre-commit ty-check-install and uv-lock-check-install; new Installer CI workflow.
  • README: new Cursor, Windsurf, Shared skills, and Windows install sections; jq dropped from prereqs; env var reference table.

Test plan

  • make lint && make test (156 tests: 120 installer + 36 plugin)
  • make install-opencode PROJECT=/tmp/x then re-run; idempotent
  • OpenCode parity diff against legacy scripts/install-opencode.sh (byte-identical: opencode.json, AGENTS.md, commands/)
  • make install-cursor against real ~/.cursor with pre-existing mcpd MCP entry; mcpd preserved through install + uninstall
  • OpenCode env field hand-added to MCP entry survives re-install (merge-not-replace)
  • OpenCode $schema preserved on existing files; seeded on fresh creation only
  • make install-windsurf against real ~/.codeium/windsurf (live install + uninstall, idempotent)
  • Multi-target dedup: --target opencode --target cursor installs shared skills exactly once
  • make uninstall-cursor/opencode/windsurf cleanly removes only cq entries; user-added siblings preserved
  • Manual follow-up: make install-claude / make uninstall-claude round-trip (blocked locally by an unrelated claude plugin install cache cleanup bug from a stale uv venv; the marketplace name fix is verified independently via direct CLI)
  • Windows 11 verification pass (deferred to maintainer once draft PR is up; pull branch and run .\scripts\install.ps1 install --target ...)

Co-authored-by: Jonathan Kingston 338988+jonathanKingston@users.noreply.github.com

@peteski22 peteski22 marked this pull request as ready for review April 8, 2026 20:23
@peteski22 peteski22 requested a review from Copilot April 8, 2026 20:25
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the legacy OpenCode-only bash installer with a stdlib-only Python installer project under scripts/install/ that supports OpenCode, Cursor, Windsurf, and Claude Code (marketplace wrapper), adds Windows-friendly entrypoints, and updates docs/CI accordingly.

Changes:

  • Introduces a new scripts/install Python project (cq_install) with idempotent primitives (JSON merge, hook entries, markdown blocks, manifest-tracked copies) and per-host adapters.
  • Updates plugin bootstrap/runtime behavior to use a shared per-user runtime cache and adds a Cursor hook helper + tests.
  • Refreshes Makefile targets, docs, pre-commit hooks, and adds an “Installer CI” workflow.

Reviewed changes

Copilot reviewed 50 out of 54 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
server/backend/tests/test_store.py Minor test formatting change (single-line SQL fetch).
scripts/test-install-opencode.sh Removes legacy bash-based OpenCode installer test script.
scripts/install/uv.lock Adds installer uv lockfile for reproducible dev deps.
scripts/install/tests/test_opencode_commands.py Unit tests for OpenCode command frontmatter transform.
scripts/install/tests/test_hosts_windsurf.py Tests Windsurf host install/uninstall behavior.
scripts/install/tests/test_hosts_registry.py Tests host registry and feature flags.
scripts/install/tests/test_hosts_opencode.py Tests OpenCode host install/uninstall behavior + schema seeding/merge behavior.
scripts/install/tests/test_hosts_cursor.py Tests Cursor host config/rule/hooks install/uninstall behavior.
scripts/install/tests/test_hosts_claude.py Tests Claude marketplace wrapper (subprocess calls + dry-run).
scripts/install/tests/test_context.py Tests core installer datatypes and shared-skills dedup logic.
scripts/install/tests/test_common_symlink.py Tests symlink_tree primitive.
scripts/install/tests/test_common_markdown.py Tests markdown block upsert/remove primitives.
scripts/install/tests/test_common_json.py Tests JSON merge-not-replace upsert/remove primitives.
scripts/install/tests/test_common_hooks.py Tests hooks.json entry upsert/remove primitives.
scripts/install/tests/test_common_file.py Tests write-if-missing and remove-owned-file primitives.
scripts/install/tests/test_common_copy.py Tests manifest-tracked copy/remove tree primitives.
scripts/install/tests/test_cli.py Tests installer CLI dispatch, targeting, dry-run, and multi-target dedup.
scripts/install/tests/conftest.py Shared fixtures incl. XDG isolation + fake plugin tree.
scripts/install/tests/init.py Marks installer tests package.
scripts/install/src/cq_install/runtime.py Shared runtime root resolution helpers (XDG + Windows fallbacks).
scripts/install/src/cq_install/opencode_commands.py Implements OpenCode command transform (strip name:, add agent: build).
scripts/install/src/cq_install/manifest.py Manifest read/write + sha256 hashing helpers.
scripts/install/src/cq_install/hosts/windsurf.py Windsurf host adapter (mcp_config.json + skills/runtime handling).
scripts/install/src/cq_install/hosts/opencode.py OpenCode host adapter (opencode.json merge + AGENTS.md + command generation).
scripts/install/src/cq_install/hosts/cursor.py Cursor host adapter (mcp.json, hooks.json, rules file, runtime bundle).
scripts/install/src/cq_install/hosts/claude.py Claude host adapter (shells out to claude plugin marketplace).
scripts/install/src/cq_install/hosts/base.py Defines HostDef interface and capability flags.
scripts/install/src/cq_install/hosts/init.py Host registry + lookup helper.
scripts/install/src/cq_install/context.py Defines Action/ChangeResult/InstallContext and RunState dedup.
scripts/install/src/cq_install/content.py Shared content blobs + platform Python command + binary name helper.
scripts/install/src/cq_install/common.py Core idempotent primitives (copy manifests, json merge, hooks, markdown blocks, etc.).
scripts/install/src/cq_install/cli.py Installer CLI parser, target resolution, execution and result printing.
scripts/install/src/cq_install/main.py python -m cq_install entrypoint.
scripts/install/src/cq_install/init.py Package marker.
scripts/install/ruff.toml Ruff configuration scoped to installer project.
scripts/install/pyproject.toml Installer project metadata + dev dependency groups + pytest config.
scripts/install.ps1 PowerShell wrapper to run installer via uv on Windows.
scripts/install-opencode.sh Removes legacy OpenCode-only bash installer.
README.md Documents multi-host installer usage, Windows instructions, and env vars.
plugins/cq/uv.lock Adds pytest (and transitive deps) to plugin dev dependencies.
plugins/cq/tests/test_cursor_hook.py Adds tests for Cursor lifecycle hook helper script.
plugins/cq/tests/test_bootstrap.py Adds tests for bootstrap runtime path/version resolution behaviors.
plugins/cq/tests/conftest.py Session-scoped loader fixture for non-package hook script.
plugins/cq/tests/init.py Marks plugin tests package.
plugins/cq/scripts/bootstrap.py Switches bootstrap to shared per-user runtime bin cache + metadata-driven required version.
plugins/cq/scripts/bootstrap.json New bootstrap metadata file containing required CLI version.
plugins/cq/pyproject.toml Adds pytest group and pytest config for plugin tests.
plugins/cq/hooks/cursor/cq_cursor_hook.py New stdlib-only Cursor lifecycle hook helper.
plugins/cq/.claude-plugin/plugin.json Removes cliVersion field in favor of bootstrap metadata.
Makefile Replaces bash installer usage with cq_install targets; adds install/uninstall for Cursor/Windsurf/all; adds lint/test targets for installer + plugin tests.
docs/architecture.md Documents the new multi-host installer architecture at a high level.
DEVELOPMENT.md Updates repo structure and prerequisites (drops jq; adds scripts/install).
.pre-commit-config.yaml Adds installer ty-check + uv lock check hooks.
.github/workflows/ci-install.yaml New CI workflow to lint/test installer and plugin hook tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread README.md Outdated
Comment thread scripts/install/src/cq_install/cli.py
Comment thread scripts/install/src/cq_install/common.py Outdated
Comment thread scripts/install/src/cq_install/common.py
Comment thread scripts/install/src/cq_install/common.py
Comment thread scripts/install/src/cq_install/hosts/claude.py Outdated
@peteski22 peteski22 marked this pull request as draft April 8, 2026 21:02
peteski22 added 25 commits April 9, 2026 08:56
Add empty uv-managed Python project at scripts/install/ with the
package skeleton, ruff config, and pytest wiring. Follow-up tasks
implement the primitive library, hosts, and CLI.
Define the shared types every primitive and host returns or
consumes: Action enum, frozen ChangeResult, InstallContext with
all resolved paths, and RunState for in-run dedup of shared steps.
JSON merge-not-replace primitive that preserves user-added sibling
fields (e.g. `env`) when updating managed keys (`command`, `args`).
Generalises the #238 stale-replace fix into the
primitive contract.
upsert_hook_entry handles per-hook arrays in hooks.json with
legacy command cleanup and extra field merging. remove_hook_entry
matches by command string and prunes empty hook lists.
Arrange public primitives alphabetically (remove_hook_entry,
remove_json_entry, upsert_hook_entry, upsert_json_entry), then
private helpers alphabetically (_load_json, _walk_or_create,
_write_json), so it's obvious where to insert new primitives.
upsert_markdown_block / remove_markdown_block manage delimited
sections in shared markdown files (e.g. the CQ block in AGENTS.md)
without disturbing surrounding content. Skipped result is returned
when start/end markers are imbalanced rather than guessing.
copy_tree records hash-tracked manifest entries so re-runs detect
unchanged files, refresh stale ones, and clean up files removed
from the source. remove_copied_tree skips files modified by the
user since install (hash mismatch) with a SKIPPED result.
Three small primitives that round out common.py: symlink_tree
for opt-out skill installs, write_if_missing for the Cursor rule
file (user-editable, never overwritten), and remove_owned_file
which compares content hash on uninstall and skips user edits.
Skeleton host adapters and a name->instance registry. install /
uninstall remain NotImplementedError; subsequent tasks port the
actual logic per host.
Host-specific strings belong beside the host that uses them, not
in a shared content module that turns into a junk drawer as new
IDEs are added. content.py now only holds genuinely shared blobs
(the AGENTS.md CQ block + markers). CURSOR_RULE_CONTENT moves to
hosts/cursor.py, establishing the pattern for future hosts.
Hosts call ctx.run_state.ensure_shared_skills(ctx) at the top of
install(); the second and subsequent calls within the same
invocation are no-ops, so multi-target runs install the shared
skill commons exactly once.
Replace the shell-based logic in scripts/install-opencode.sh with
a Python host adapter that exercises every primitive: shared
skills (or host-isolated copy), MCP merge-not-replace, command
file generation, AGENTS.md block management. The shell installer
itself is removed in a later task once parity is verified locally.
argparse-driven `python -m cq_install (install|uninstall)` with
repeatable --target, --global/--project mutual exclusion,
--host-isolated-skills, and --dry-run. Targets are processed in
order, fail-fast on first error.
Two changes surfaced by the Task 12 parity diff:

1. Seed $schema on fresh opencode.json creation. The URL gives
   OpenCode autocomplete and validation; write it when creating
   the file from scratch but never rewrite on existing files so
   user overrides survive re-installs.

2. Replace sys.executable with PYTHON_COMMAND in the MCP config.
   sys.executable captures the installer's own venv python under
   `uv run`, which is tied to the repo location and gets stale
   if the user moves or deletes the repo. PYTHON_COMMAND is a
   literal name (`python3` on POSIX, `python` on Windows per
   python.org docs) that PATH-resolves at OpenCode's invocation
   time. Detection uses platform.system() == "Windows" to match
   the existing bootstrap.py idiom.

PYTHON_COMMAND lives in cq_install.content so Cursor, Windsurf,
and Claude host adapters (Tasks 13-15) reuse the same source of
truth. OPENCODE_SCHEMA_URL is a local constant in hosts/opencode.py.

Parity diff against scripts/install-opencode.sh is now byte-identical
(opencode.json, AGENTS.md, commands/).
CursorHost writes three things:
- mcp.json: mcpServers.cq entry with PYTHON_COMMAND + bootstrap.
- rules/cq.mdc: created on first install via write_if_missing,
  never overwritten on re-runs (user-editable rule file).
- hooks.json: four lifecycle hooks (sessionStart, postToolUse,
  postToolUseFailure, stop) each pointing at cq_cursor_hook.py
  with --mode and a per-target --state-dir flag.

_hook_command produces the shell-executable string Cursor wants
by picking subprocess.list2cmdline on Windows and shlex.join on
POSIX, so the quoting matches each platform's shell rules.

Also flips the xfailed CLI multi-target test to assert success:
installing opencode + cursor in one invocation now succeeds, and
the shared skill commons is installed exactly once via the
RunState.ensure_shared_skills dedup helper.
WindsurfHost writes mcpServers.cq to mcp_config.json under the
Windsurf global config dir (~/.codeium/windsurf/, confirmed on
both POSIX and Windows 11). Per Windsurf docs there is no
per-project MCP config, so supports_project = False and the
class raises NotImplementedError from project_target.

Shared skills install via RunState.ensure_shared_skills; the
--host-isolated-skills opt-out copies into <target>/skills.
Closes #154.
Unlike the other hosts, Claude Code ships its own plugin
marketplace so this adapter is a thin shell-out rather than a
filesystem writer. install runs `claude plugin marketplace add`
followed by `claude plugin install`; uninstall runs `claude
plugin marketplace remove`. Dry-run skips subprocess invocation.

Refuses to run if `claude` is not on PATH and prints a clear
pointer. global_target returns a sentinel path that is never
read, and both supports_project and supports_host_isolated are
False since neither apply to the marketplace flow.
Stdlib-only Python script invoked by Cursor's session-start,
post-tool-use, post-tool-use-failure, and stop hooks. Captures
failed tool calls per session and emits a summary on stop. State
dir is passed explicitly via --state-dir, scoped per install
target. Sweeps state files older than 24h on session start.

Tested with 14 pytest cases via a session-scoped `hook` fixture
in conftest.py that loads the script once per test session
(importlib.util.spec_from_file_location since the hook isn't a
package member). Coverage: _truncate edges, _format_tool_input
for Shell/Edit/Write/Bash/Read/unknown tools, session-start
state creation and TTL sweep, post-tool-use-failure happy path
and isInterrupt short-circuit, stop with and without prior
failure state.

pytest is added to the plugin's dev dependency group so the
tests run under `make test-plugin` once that target lands in
Task 17.
Wire `cd plugins/cq && uv run pytest` into the aggregate test
target so the Cursor hook tests run under `make test`. Also
list it in the help block alongside test-cli, test-sdk-*, etc.
Wire scripts/install/ into the aggregate setup, lint, and test
targets so `make lint && make test` covers the new installer.
Help block updated to list the new targets.
cli.py's default plugin root resolver was using parents[3] which
walks to `scripts/` not the repo root, so it resolved to
`scripts/plugins/cq` (non-existent). Should be parents[4].

Caught by smoke-testing `make install-opencode PROJECT=/tmp/x`:
commands/ wasn't being generated because the plugin tree
couldn't be found at the stale path. Tests missed it because
every CLI test sets CQ_INSTALL_PLUGIN_ROOT explicitly via the
fake_repo fixture, so the fallback path was never exercised.

Adds a unit test that calls _resolve_plugin_root() directly
with the env var unset and asserts the result contains the
plugin's bootstrap script, commands dir, and skills dir.
install-opencode no longer shells out to install-opencode.sh;
it now calls `python -m cq_install install --target opencode`.
Added install-cursor, install-windsurf, install-claude, and
install-all with matching uninstall-* counterparts, all
following the same PROJECT=/path pattern. Help block lists the
new targets grouped by host.

Windsurf has no per-project MCP config, so install-windsurf /
uninstall-windsurf always run with --global and print a note if
the caller passed PROJECT=. install-all with PROJECT= routes
the project-capable hosts through --project and then runs
Windsurf separately with --global, prefixed by the same note.
Mirrors the existing checks for plugins/cq, sdk/python, and
server/backend. ty-check-install runs `uvx ty check src/cq_install`
in the installer's uv venv; uv-lock-check-install enforces lockfile
freshness on any change to pyproject.toml / uv.lock.
Switch code comments and docstrings to US English for consistency
(docs and PR bodies stay whatever the author writes). Also
capitalize "Markdown" as a proper noun.

- common.py: "markdown" -> "Markdown" and "synthesise" -> "synthesize"
  in upsert_markdown_block / remove_markdown_block docstrings.
- opencode_commands.py: "flavoured" -> "flavored".
- test_common_markdown.py: "markdown" -> "Markdown" in module
  docstring.
- cq_cursor_hook.py: "Initialise" -> "Initialize" in module
  docstring.
No behavior change: the remove_* calls in CursorHost.uninstall
and WindsurfHost.uninstall build a single list literal instead
of append()ing one at a time. CursorHost's hook-loop appends
stay as-is since they run inside a for loop.
peteski22 and others added 14 commits April 9, 2026 08:56
Replace magic strings inline in the four host adapters with
module-level constants so future changes have a single touch
point and reading the install/uninstall flow doesn't require
mental substitution.

Per-host constants live in each hosts/<host>.py since the values
are host-specific (with a little copying preferred over premature
unification when two hosts happen to share the same value). One
shared constant CQ_MCP_KEY = "cq" lives in content.py because
all three MCP-writing hosts genuinely use the same leaf key name
for the cq entry (only the wrapping key differs: "mcp" vs
"mcpServers").

OpenCode gains a second behaviour change: global_target() now
honours the OPENCODE_CONFIG_DIR environment variable the same
way OpenCode itself does
(https://opencode.ai/docs/config/). If a user has set it, the
installer writes to the overridden location so the installer and
OpenCode agree on where opencode.json lives. Default path stays
~/.config/opencode. Two new tests cover the default and
env-override branches. Also fixes a misleading "XDG-style"
comment: OpenCode does not honour XDG_CONFIG_HOME.

Extracted constants, by host:
- content.py: CQ_MCP_KEY
- opencode.py: OPENCODE_AGENTS_FILE, OPENCODE_COMMANDS_DIR,
  OPENCODE_CONFIG_DIR_ENV, OPENCODE_CONFIG_FILE,
  OPENCODE_GLOBAL_TARGET, OPENCODE_MCP_KEY,
  OPENCODE_PROJECT_TARGET, OPENCODE_SKILLS_DIR
- cursor.py: CURSOR_HOOKS_FILE, CURSOR_HOOK_SCRIPT_RELPATH,
  CURSOR_MCP_FILE, CURSOR_MCP_SERVERS_KEY, CURSOR_RULE_RELPATH,
  CURSOR_SKILLS_DIR, CURSOR_STATE_DIR, CURSOR_TARGET_DIR
- windsurf.py: WINDSURF_GLOBAL_TARGET, WINDSURF_MCP_FILE,
  WINDSURF_MCP_SERVERS_KEY, WINDSURF_SKILLS_DIR
- claude.py: CLAUDE_CLI, CLAUDE_PLUGIN_NAME, CLAUDE_PLUGIN_PACKAGE
Lint and test scripts/install/ and the plugin's Cursor hook
tests on changes to either component. Mirrors ci-plugin.yaml
shape: setup-uv then make setup-install/setup-plugin then
lint-install/test-install/test-plugin. Ubuntu runner only;
Windows verification is a manual step in Task 24.
scripts/install.ps1 forwards arguments to `uv run python -m
cq_install` from the scripts/install directory, giving Windows
users the same one-liner ergonomics as `make install-*` on POSIX
(Windows doesn't ship make).

Errors out early if uv is not on PATH. Pushes into the installer
dir before running so python -m cq_install resolves the package
correctly, and pops back on exit regardless of success or
failure. Returns the installer's exit code.
…skills

- README: drop jq prereq; add Python 3.11+ to the prereq line; add
  Cursor, Windsurf, Shared skills, and Windows sections to the
  installation guide. Windows section lists the PowerShell wrapper
  and the home-relative config paths.
- DEVELOPMENT.md: drop jq from prereqs; add scripts/install to the
  repository structure table.
- docs/architecture.md: append a short "Multi-host installer" note
  to section 5 explaining scripts/install's shape and extension
  point (one new file per host under cq_install/hosts/).
scripts/install-opencode.sh and its test harness are superseded
by the cq_install Python installer at scripts/install/. Task 12's
parity diff confirmed byte-identical output against the shell
version on macOS (opencode.json, AGENTS.md, commands/), and the
Makefile install-opencode target now routes through python -m
cq_install, so nothing references the shell scripts any more.

Windows support is a bonus from the rewrite: the shell script
never worked natively on Windows, only under WSL or Git Bash.
`claude plugin marketplace remove` takes the derived marketplace
name (the repo portion of the GitHub slug), not the full slug.
Add succeeds with `mozilla-ai/cq` because it parses the source;
remove fails with "Marketplace 'mozilla-ai/cq' not found" and
needs the short name `cq` instead.

Split the existing CLAUDE_PLUGIN_PACKAGE constant into
CLAUDE_MARKETPLACE_SOURCE (used by add) and CLAUDE_MARKETPLACE_NAME
(used by remove). Removing the marketplace also unregisters any
plugins installed from it, so uninstall stays at one CLI call.

Surfaced by manually running `make uninstall-claude` against a
real Claude Code install during the Task 24 verification pass.
…ed XDG runtime

* copy runtime assets for Cursor/Windsurf/OpenCode into a shared per-user runtime path
* point non-Claude MCP/hook commands to the shared runtime bootstrap path
* align runtime path resolution with XDG_DATA_HOME semantics across platforms
* make Claude bootstrap prefer CLAUDE_PLUGIN_ROOT for installed plugin resolution
* reuse valid symlinked cq binaries in bootstrap cache fast-path (no unnecessary relink)
* add and update tests for shared runtime paths, bootstrap root resolution, and symlink cache reuse
* document installer/plugin environment variables, including Windows-specific fallbacks in a separate table
…talls

* store shared runtime version metadata in scripts/bootstrap.json
* remove non-Claude runtime dependence on .claude-plugin/plugin.json
* keep Claude manifest focused on Claude plugin config only
* make installer output include per-hook details for Cursor hooks.json updates
* reuse typed bootstrap helpers and keep bootstrap metadata in snake_case
* update tests for neutral bootstrap metadata and shared runtime expectations
Replace Python bootstrap wrapper with direct binary launch for Cursor,
Windsurf, and OpenCode to fix MCP startup timeouts on Windows. The
bootstrap wrapper was causing stdin/stdout buffering issues.

- Add cq_binary_name() helper for platform-specific binary name
- Update MCP configs to invoke binary directly with "mcp" argument
- Fix Windows line ending mismatch in text file writes to preserve hashes
- Update host adapter tests accordingly

Claude plugin behavior unchanged.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Move ensure_binary/download/check_version/path helpers out of
bootstrap.py into a sibling stdlib-only cq_binary.py so the
installer can reuse the same code without a subprocess dance.

bootstrap.py shrinks to a ~37-line Claude launcher that imports
cq_binary and execvp's into `cq mcp`. plugin.json still points at
bootstrap.py, so the launch path is unchanged from Claude's
perspective.

load_required_version now takes an explicit metadata_path so tests
can isolate with tmp_path instead of mutating the real
plugins/cq/scripts/bootstrap.json in place. ensure_binary drops its
system parameter (callers do not need it); download still takes one
for independent testability.

Tests move to plugins/cq/tests/test_cq_binary.py covering XDG
fallback on every platform, version helpers, and the fast / system
/ download paths. test_bootstrap.py shrinks to a single smoke test
of the main() wrapper.
Cursor, Windsurf, and OpenCode had their MCP configs pointing at
runtime_root/bin/cq but nothing in the installer ever placed a
binary there. On a clean machine the MCP server failed to start.
Add a shared fetch step that dynamically loads
plugins/cq/scripts/cq_binary.py via importlib.util and calls its
ensure_binary before any host writes its MCP config.

The fetch runs once per installer invocation via RunState so
install-all does not re-fetch per host. Under --dry-run the step
reports "would fetch cq vX.Y.Z" without touching disk.

Non-Claude runtime bundles shrink to only what each host actually
needs: Windsurf and OpenCode no longer copy anything into
runtime_root, and Cursor's bundle reduces to just the lifecycle
hook script. Stale bootstrap.py/bootstrap.json copies from
previous installs are cleaned up by the existing manifest-tracked
copy_tree diff on the next Cursor install.

Cleanup bundled in:
- InstallContext.bootstrap_path removed (dead field).
- Three _runtime_root(ctx) stubs in host adapters deleted.
- _copy_selected_paths / _CQ_RUNTIME_MANIFEST renamed to drop
  the underscore (they were imported across modules).
- _CQ_RUNTIME_BASE_RELPATHS deleted (no longer needed).
Five defensive fixes:

- cli.py: --host-isolated-skills is now rejected with a clear
  error when any target host declares
  supports_host_isolated = False, instead of being silently
  ignored.
- common._load_json: wraps json.JSONDecodeError as RuntimeError
  naming the file, so malformed user config surfaces an
  actionable message instead of a traceback.
- common.upsert_json_entry: raises ValueError naming the file
  and dotted path when the existing leaf is not a dict, instead
  of crashing at dict(existing).
- common.upsert_hook_entry: validates that the top level is an
  object, hooks is an object, and hooks[<name>] is a list;
  raises ValueError with the file and offending key on
  mismatch.
- hosts/claude.py: _run_each now captures stdout/stderr via
  subprocess.run(..., capture_output=True, text=True) and
  includes returncode + stderr (+ stdout if present) in the
  raised RuntimeError, so subprocess failures are diagnosable
  in CI.
@peteski22 peteski22 force-pushed the feature/multi-host-installer branch from b9130ae to f21d507 Compare April 9, 2026 07:56
@peteski22 peteski22 requested a review from Copilot April 9, 2026 07:57
@peteski22 peteski22 marked this pull request as ready for review April 9, 2026 07:58
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 54 out of 58 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/install/src/cq_install/common.py
Comment thread scripts/install/src/cq_install/common.py
Comment thread scripts/install/src/cq_install/common.py Outdated
Comment thread scripts/install/src/cq_install/common.py
Comment thread scripts/install/src/cq_install/common.py Outdated
Comment thread scripts/install/src/cq_install/hosts/opencode.py Outdated
Six fixes from the second round of Copilot review on PR #254:

- remove_hook_entry: returns SKIPPED with a detail message when
  the file contains non-dict top level or non-dict hooks key,
  instead of crashing during uninstall.
- symlink_tree: passes target_is_directory to Path.symlink_to so
  directory symlinks are created correctly on Windows.
- upsert_markdown_block: returns UPDATED (not CREATED) when
  appending a block to an existing file.
- write_if_missing: docstring corrected to match the actual
  newline parameter value.
- _walk_or_create: raises ValueError naming the file and dotted
  path when an intermediate key exists but is not a dict, instead
  of silently overwriting user config.
- _uninstall_commands (OpenCode): compares file content against
  expected generated output before deleting; user-edited command
  files are left in place with a SKIPPED result, consistent with
  remove_owned_file and remove_copied_tree.
@peteski22 peteski22 merged commit 3454b2e into main Apr 9, 2026
7 checks passed
@peteski22 peteski22 deleted the feature/multi-host-installer branch April 9, 2026 11:22
peteski22 added a commit that referenced this pull request Apr 9, 2026
Commit 3454b2e squash-merged PR #254 with a malformed co-author
trailer. This empty commit corrects the attribution for Jonathan
Kingston's contribution to the multi-host installer (see #166).

Co-authored-by: Jonathan Kingston <338988+jonathanKingston@users.noreply.github.com>
peteski22 added a commit that referenced this pull request Apr 9, 2026
Commit 3454b2e squash-merged PR #254 with a malformed co-author
trailer. This empty commit corrects the attribution for Jonathan
Kingston's contribution to the multi-host installer (see #166).

Co-authored-by: Jonathan Kingston <338988+jonathanKingston@users.noreply.github.com>
Copilot AI pushed a commit that referenced this pull request Apr 9, 2026
feat(installer): multi-host installer for Cursor, Windsurf, OpenCode, and Claude

Replace the legacy OpenCode-only shell installer with a stdlib-only Python installer at scripts/install/ supporting four hosts. Drops jq as a prerequisite.

Installer core:
  - Idempotent primitives for JSON merge, hook entries, Markdown blocks,
    manifest-tracked file copies, and symlinks. --dry-run across all.
  - Per-host adapters under cq_install/hosts/ with a shared RunState for
    dedup (shared skills installed once, binary fetched once).
  - Config write hardening: malformed JSON, non-dict leaves, and
    unexpected hook shapes raise clear errors naming the file.

Binary management:
  - New plugins/cq/scripts/cq_binary.py (stdlib-only) owns the binary
    download, version check, and cache logic. Single source of truth
    consumed by both the Claude plugin bootstrap and the installer.
  - Non-Claude hosts invoke the cq binary directly for MCP, fixing
    startup timeouts on Windows caused by Python stdio buffering.
  - bootstrap.py shrinks to a thin Claude-only launcher.

Hosts:
  - Cursor: mcp.json, rules/cq.mdc, four lifecycle hooks with
    platform-aware shell escaping, shared runtime hook script.
  - Windsurf: mcp_config.json, shared skills.
  - OpenCode: opencode.json with schema seeding, AGENTS.md block,
    generated commands with user-edit protection on uninstall.
  - Claude: thin marketplace wrapper (add/install/remove).

Build:
  - Makefile targets for all hosts plus install-all/uninstall-all.
  - PowerShell wrapper for Windows.
  - Installer CI workflow, pre-commit ty and uv-lock checks.

Co-authored-by: Jonathan Kingston (#166)

Closes #154
Co-authored-by: peteski22 <487783+peteski22@users.noreply.github.com>
Copilot AI pushed a commit that referenced this pull request Apr 9, 2026
Commit 3454b2e squash-merged PR #254 with a malformed co-author
trailer. This empty commit corrects the attribution for Jonathan
Kingston's contribution to the multi-host installer (see #166).

Co-authored-by: Jonathan Kingston <338988+jonathanKingston@users.noreply.github.com>
Co-authored-by: peteski22 <487783+peteski22@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implementing into windsurf IDE

2 participants