From b1223034c3acbf2893593dfe8ae5fc5bcb352e09 Mon Sep 17 00:00:00 2001 From: ARRRRNY Date: Sun, 31 May 2026 11:38:27 +0300 Subject: [PATCH] feat: add Zed integration --- docs/index.md | 2 +- docs/reference/integrations.md | 1 + src/specify_cli/commands/init.py | 8 ++- src/specify_cli/extensions.py | 3 ++ src/specify_cli/integrations/__init__.py | 2 + src/specify_cli/integrations/zed/__init__.py | 41 +++++++++++++++ .../test_integration_subcommand.py | 1 + tests/integrations/test_integration_zed.py | 51 +++++++++++++++++++ tests/integrations/test_registry.py | 2 +- 9 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 src/specify_cli/integrations/zed/__init__.py create mode 100644 tests/integrations/test_integration_zed.py diff --git a/docs/index.md b/docs/index.md index c8b02d98cb..486193e823 100644 --- a/docs/index.md +++ b/docs/index.md @@ -31,7 +31,7 @@ Define what to build before building it. Rich templates, quality checklists, and ### Use any coding agent -30 integrations — Copilot, Gemini, Codex, Windsurf, Claude, Forge, Kiro, and more. Switch freely between agents with a single command. No lock-in. +31 integrations — Copilot, Gemini, Codex, Windsurf, Zed, Claude, Forge, Kiro, and more. Switch freely between agents with a single command. No lock-in. Run `specify init` with your agent of choice and Spec Kit sets up the right command files, context rules, and directory structures automatically. If your agent isn't listed, the `generic` integration is an escape hatch for any tool. diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index ec6c894652..ab34bf5846 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -35,6 +35,7 @@ The Specify CLI supports a wide range of AI coding agents. When you run `specify | [Tabnine CLI](https://docs.tabnine.com/main/getting-started/tabnine-cli) | `tabnine` | | | [Trae](https://www.trae.ai/) | `trae` | Skills-based integration; skills are installed automatically | | [Windsurf](https://windsurf.com/) | `windsurf` | | +| [Zed](https://zed.dev/) | `zed` | Skills-based integration; installs skills into `.agents/skills` and invokes them as `/speckit-` | | Generic | `generic` | Bring your own agent — use `--integration generic --integration-options="--commands-dir "` for AI coding agents not listed above | ## List Available Integrations diff --git a/src/specify_cli/commands/init.py b/src/specify_cli/commands/init.py index e5dc47e98c..e034affddf 100644 --- a/src/specify_cli/commands/init.py +++ b/src/specify_cli/commands/init.py @@ -726,7 +726,8 @@ def init( cursor_agent_skill_mode = selected_ai == "cursor-agent" and (ai_skills or _is_skills_integration) copilot_skill_mode = selected_ai == "copilot" and _is_skills_integration devin_skill_mode = selected_ai == "devin" - native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode + zed_skill_mode = selected_ai == "zed" and _is_skills_integration + native_skill_mode = codex_skill_mode or claude_skill_mode or kimi_skill_mode or agy_skill_mode or trae_skill_mode or cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode or zed_skill_mode if codex_skill_mode and not ai_skills: steps_lines.append(f"{step_num}. Start Codex in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]") @@ -740,6 +741,9 @@ def init( if devin_skill_mode: steps_lines.append(f"{step_num}. Start Devin in this project directory; spec-kit skills were installed to [cyan].devin/skills[/cyan]") step_num += 1 + if zed_skill_mode: + steps_lines.append(f"{step_num}. Start Zed in this project directory; spec-kit skills were installed to [cyan].agents/skills[/cyan]") + step_num += 1 usage_label = "skills" if native_skill_mode else "slash commands" def _display_cmd(name: str) -> str: @@ -749,7 +753,7 @@ def _display_cmd(name: str) -> str: return f"/speckit-{name}" if kimi_skill_mode: return f"/skill:speckit-{name}" - if cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode: + if cursor_agent_skill_mode or copilot_skill_mode or devin_skill_mode or zed_skill_mode: return f"/speckit-{name}" return f"/speckit.{name}" diff --git a/src/specify_cli/extensions.py b/src/specify_cli/extensions.py index 5a595fbffa..a11eedcbdb 100644 --- a/src/specify_cli/extensions.py +++ b/src/specify_cli/extensions.py @@ -2413,6 +2413,7 @@ def _render_hook_invocation(self, command: Any) -> str: claude_skill_mode = selected_ai == "claude" and bool(init_options.get("ai_skills")) kimi_skill_mode = selected_ai == "kimi" cursor_skill_mode = selected_ai == "cursor-agent" and bool(init_options.get("ai_skills")) + zed_skill_mode = selected_ai == "zed" and bool(init_options.get("ai_skills")) skill_name = self._skill_name_from_command(command_id) if codex_skill_mode and skill_name: @@ -2423,6 +2424,8 @@ def _render_hook_invocation(self, command: Any) -> str: return f"/skill:{skill_name}" if cursor_skill_mode and skill_name: return f"/{skill_name}" + if zed_skill_mode and skill_name: + return f"/{skill_name}" return f"/{command_id}" diff --git a/src/specify_cli/integrations/__init__.py b/src/specify_cli/integrations/__init__.py index ad1440d074..c3d7499ff3 100644 --- a/src/specify_cli/integrations/__init__.py +++ b/src/specify_cli/integrations/__init__.py @@ -78,6 +78,7 @@ def _register_builtins() -> None: from .trae import TraeIntegration from .vibe import VibeIntegration from .windsurf import WindsurfIntegration + from .zed import ZedIntegration # -- Registration (alphabetical) -------------------------------------- _register(AgyIntegration()) @@ -111,6 +112,7 @@ def _register_builtins() -> None: _register(TraeIntegration()) _register(VibeIntegration()) _register(WindsurfIntegration()) + _register(ZedIntegration()) _register_builtins() diff --git a/src/specify_cli/integrations/zed/__init__.py b/src/specify_cli/integrations/zed/__init__.py new file mode 100644 index 0000000000..004cfc57f6 --- /dev/null +++ b/src/specify_cli/integrations/zed/__init__.py @@ -0,0 +1,41 @@ +"""Zed editor integration. — skills-based agent. + +Zed uses the ``.agents/skills/speckit-/SKILL.md`` layout so Spec Kit +commands are exposed as project-local skills that can be invoked from Zed's +slash-command menu. +""" + +from __future__ import annotations + +from ..base import IntegrationOption, SkillsIntegration + + +class ZedIntegration(SkillsIntegration): + """Integration for Zed editor skills.""" + + key = "zed" + config = { + "name": "Zed", + "folder": ".agents/", + "commands_subdir": "skills", + "install_url": None, + "requires_cli": False, + } + registrar_config = { + "dir": ".agents/skills", + "format": "markdown", + "args": "$ARGUMENTS", + "extension": "/SKILL.md", + } + context_file = "AGENTS.md" + + @classmethod + def options(cls) -> list[IntegrationOption]: + return [ + IntegrationOption( + "--skills", + is_flag=True, + default=True, + help="Install as agent skills (default for Zed)", + ), + ] diff --git a/tests/integrations/test_integration_subcommand.py b/tests/integrations/test_integration_subcommand.py index f40adb7ae9..42a088f541 100644 --- a/tests/integrations/test_integration_subcommand.py +++ b/tests/integrations/test_integration_subcommand.py @@ -93,6 +93,7 @@ def test_list_shows_available_integrations(self, tmp_path): # Should show multiple integrations assert "claude" in result.output assert "gemini" in result.output + assert "zed" in result.output def test_list_shows_multi_install_safe_status(self, tmp_path): project = _init_project(tmp_path, "claude") diff --git a/tests/integrations/test_integration_zed.py b/tests/integrations/test_integration_zed.py new file mode 100644 index 0000000000..f3256c7fc7 --- /dev/null +++ b/tests/integrations/test_integration_zed.py @@ -0,0 +1,51 @@ +"""Tests for ZedIntegration.""" + +import json + +from specify_cli.integrations import get_integration + +from .test_integration_base_skills import SkillsIntegrationTests + + +class TestZedIntegration(SkillsIntegrationTests): + KEY = "zed" + FOLDER = ".agents/" + COMMANDS_SUBDIR = "skills" + REGISTRAR_DIR = ".agents/skills" + CONTEXT_FILE = "AGENTS.md" + + def test_requires_cli_is_false(self): + """Zed is IDE-based; requires_cli must remain False.""" + i = get_integration(self.KEY) + assert i is not None + assert i.config is not None + assert i.config["requires_cli"] is False + + +class TestZedHookInvocations: + """Zed hook messages should reference slash-invokable skills.""" + + def test_hooks_render_skill_invocation(self, tmp_path): + from specify_cli.extensions import HookExecutor + + project = tmp_path / "zed-hooks" + project.mkdir() + init_options = project / ".specify" / "init-options.json" + init_options.parent.mkdir(parents=True, exist_ok=True) + init_options.write_text(json.dumps({"ai": "zed", "ai_skills": True})) + + hook_executor = HookExecutor(project) + message = hook_executor.format_hook_message( + "before_plan", + [ + { + "extension": "test-ext", + "command": "speckit.plan", + "optional": False, + } + ], + ) + + assert "Executing: `/speckit-plan`" in message + assert "EXECUTE_COMMAND: speckit.plan" in message + assert "EXECUTE_COMMAND_INVOCATION: /speckit-plan" in message diff --git a/tests/integrations/test_registry.py b/tests/integrations/test_registry.py index 1b36501056..b6f7439254 100644 --- a/tests/integrations/test_registry.py +++ b/tests/integrations/test_registry.py @@ -27,7 +27,7 @@ # Stage 4 — TOML integrations "gemini", "tabnine", # Stage 5 — skills, generic & option-driven integrations - "codex", "kimi", "agy", "generic", + "codex", "kimi", "agy", "zed", "generic", ]