Skip to content

Commit d0df42e

Browse files
committed
Stage 3: Standard markdown integrations — 19 agents migrated to plugin architecture
Migrate all standard markdown integrations to self-contained subpackages under integrations/. Each subclasses MarkdownIntegration with config-only overrides (~10 lines per __init__.py). Integrations migrated (19): claude, qwen, opencode, junie, kilocode, auggie, roo, codebuddy, qodercli, amp, shai, bob, trae, pi, iflow, kiro-cli, windsurf, vibe, cursor-agent Changes: - Create integrations/<key>/ subpackage with __init__.py and scripts/ (update-context.sh, update-context.ps1) for each integration - Register all 19 in INTEGRATION_REGISTRY (20 total with copilot) - MarkdownIntegration.setup() processes templates (replaces {SCRIPT}, {ARGS}, __AGENT__; strips frontmatter blocks; rewrites paths) - Extract install_scripts() to IntegrationBase; refactor copilot to use it - Generalize --ai auto-promote from copilot-only to registry-driven: any integration registered in INTEGRATION_REGISTRY auto-promotes. Unregistered agents (gemini, tabnine, codex, kimi, agy, generic) continue through the legacy --ai path unchanged. - Fix cursor/cursor-agent key mismatch in CommandRegistrar.AGENT_CONFIGS - Add missing vibe entry to CommandRegistrar.AGENT_CONFIGS - Update kiro alias test to reflect auto-promote behavior Testing: - Per-agent test files (test_integration_<agent>.py) with shared mixin - 1316 tests passing, 0 failures - Complete file inventory tests for both sh and ps variants - Byte-for-byte validated against v0.4.3 release packages (684 files)
1 parent 3899dcc commit d0df42e

85 files changed

Lines changed: 1591 additions & 45 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/specify_cli/__init__.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,7 +1974,7 @@ def init(
19741974
console.print("[yellow]Use:[/yellow] --integration for the new integration system, or --ai for the legacy path")
19751975
raise typer.Exit(1)
19761976

1977-
# Auto-promote: --ai copilot → integration path with a nudge
1977+
# Auto-promote: --ai <key> → integration path with a nudge (if registered)
19781978
use_integration = False
19791979
if integration:
19801980
from .integrations import INTEGRATION_REGISTRY, get_integration
@@ -1987,14 +1987,14 @@ def init(
19871987
use_integration = True
19881988
# Map integration key to the ai_assistant variable for downstream compatibility
19891989
ai_assistant = integration
1990-
elif ai_assistant == "copilot":
1990+
elif ai_assistant:
19911991
from .integrations import get_integration
1992-
resolved_integration = get_integration("copilot")
1992+
resolved_integration = get_integration(ai_assistant)
19931993
if resolved_integration:
19941994
use_integration = True
19951995
console.print(
1996-
"[dim]Tip: Use [bold]--integration copilot[/bold] instead of "
1997-
"--ai copilot. The --ai flag will be deprecated in a future release.[/dim]"
1996+
f"[dim]Tip: Use [bold]--integration {ai_assistant}[/bold] instead of "
1997+
f"--ai {ai_assistant}. The --ai flag will be deprecated in a future release.[/dim]"
19981998
)
19991999

20002000
if project_name == ".":

src/specify_cli/agents.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class CommandRegistrar:
4343
"args": "$ARGUMENTS",
4444
"extension": ".agent.md"
4545
},
46-
"cursor": {
46+
"cursor-agent": {
4747
"dir": ".cursor/commands",
4848
"format": "markdown",
4949
"args": "$ARGUMENTS",
@@ -162,6 +162,12 @@ class CommandRegistrar:
162162
"format": "markdown",
163163
"args": "$ARGUMENTS",
164164
"extension": ".md"
165+
},
166+
"vibe": {
167+
"dir": ".vibe/prompts",
168+
"format": "markdown",
169+
"args": "$ARGUMENTS",
170+
"extension": ".md"
165171
}
166172
}
167173

src/specify_cli/integrations/__init__.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,5 +42,53 @@ def _register_builtins() -> None:
4242

4343
_register(CopilotIntegration())
4444

45+
# Stage 3 — standard markdown integrations
46+
from .claude import ClaudeIntegration
47+
from .qwen import QwenIntegration
48+
from .opencode import OpencodeIntegration
49+
from .junie import JunieIntegration
50+
from .kilocode import KilocodeIntegration
51+
from .auggie import AuggieIntegration
52+
from .roo import RooIntegration
53+
from .codebuddy import CodebuddyIntegration
54+
from .qodercli import QodercliIntegration
55+
from .amp import AmpIntegration
56+
from .shai import ShaiIntegration
57+
from .bob import BobIntegration
58+
from .trae import TraeIntegration
59+
from .pi import PiIntegration
60+
from .iflow import IflowIntegration
61+
62+
_register(ClaudeIntegration())
63+
_register(QwenIntegration())
64+
_register(OpencodeIntegration())
65+
_register(JunieIntegration())
66+
_register(KilocodeIntegration())
67+
_register(AuggieIntegration())
68+
_register(RooIntegration())
69+
_register(CodebuddyIntegration())
70+
_register(QodercliIntegration())
71+
_register(AmpIntegration())
72+
_register(ShaiIntegration())
73+
_register(BobIntegration())
74+
_register(TraeIntegration())
75+
_register(PiIntegration())
76+
_register(IflowIntegration())
77+
78+
# Hyphenated package names — use importlib for kiro-cli and cursor-agent
79+
import importlib
80+
81+
kiro_mod = importlib.import_module(".kiro-cli", __package__)
82+
_register(kiro_mod.KiroCliIntegration())
83+
84+
from .windsurf import WindsurfIntegration
85+
from .vibe import VibeIntegration
86+
87+
_register(WindsurfIntegration())
88+
_register(VibeIntegration())
89+
90+
cursor_mod = importlib.import_module(".cursor-agent", __package__)
91+
_register(cursor_mod.CursorAgentIntegration())
92+
4593

4694
_register_builtins()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Amp CLI integration."""
2+
3+
from ..base import MarkdownIntegration
4+
5+
6+
class AmpIntegration(MarkdownIntegration):
7+
key = "amp"
8+
config = {
9+
"name": "Amp",
10+
"folder": ".agents/",
11+
"commands_subdir": "commands",
12+
"install_url": "https://ampcode.com/manual#install",
13+
"requires_cli": True,
14+
}
15+
registrar_config = {
16+
"dir": ".agents/commands",
17+
"format": "markdown",
18+
"args": "$ARGUMENTS",
19+
"extension": ".md",
20+
}
21+
context_file = "AGENTS.md"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# update-context.ps1 — Amp integration: create/update AGENTS.md
2+
#
3+
# Thin wrapper that delegates to the shared update-agent-context script.
4+
# Activated in Stage 7 when the shared script uses integration.json dispatch.
5+
#
6+
# Until then, this delegates to the shared script as a subprocess.
7+
8+
$ErrorActionPreference = 'Stop'
9+
10+
$repoRoot = git rev-parse --show-toplevel 2>$null
11+
if (-not $repoRoot) { $repoRoot = $PWD.Path }
12+
13+
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType amp
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bash
2+
# update-context.sh — Amp integration: create/update AGENTS.md
3+
#
4+
# Thin wrapper that delegates to the shared update-agent-context script.
5+
# Activated in Stage 7 when the shared script uses integration.json dispatch.
6+
#
7+
# Until then, this delegates to the shared script as a subprocess.
8+
9+
set -euo pipefail
10+
11+
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
12+
13+
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" amp
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Auggie CLI integration."""
2+
3+
from ..base import MarkdownIntegration
4+
5+
6+
class AuggieIntegration(MarkdownIntegration):
7+
key = "auggie"
8+
config = {
9+
"name": "Auggie CLI",
10+
"folder": ".augment/",
11+
"commands_subdir": "commands",
12+
"install_url": "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli",
13+
"requires_cli": True,
14+
}
15+
registrar_config = {
16+
"dir": ".augment/commands",
17+
"format": "markdown",
18+
"args": "$ARGUMENTS",
19+
"extension": ".md",
20+
}
21+
context_file = ".augment/rules/specify-rules.md"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# update-context.ps1 — Auggie CLI integration: create/update .augment/rules/specify-rules.md
2+
#
3+
# Thin wrapper that delegates to the shared update-agent-context script.
4+
# Activated in Stage 7 when the shared script uses integration.json dispatch.
5+
#
6+
# Until then, this delegates to the shared script as a subprocess.
7+
8+
$ErrorActionPreference = 'Stop'
9+
10+
$repoRoot = git rev-parse --show-toplevel 2>$null
11+
if (-not $repoRoot) { $repoRoot = $PWD.Path }
12+
13+
& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType auggie
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bash
2+
# update-context.sh — Auggie CLI integration: create/update .augment/rules/specify-rules.md
3+
#
4+
# Thin wrapper that delegates to the shared update-agent-context script.
5+
# Activated in Stage 7 when the shared script uses integration.json dispatch.
6+
#
7+
# Until then, this delegates to the shared script as a subprocess.
8+
9+
set -euo pipefail
10+
11+
REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
12+
13+
exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" auggie

src/specify_cli/integrations/base.py

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,53 @@ def write_file_and_record(
206206
manifest.record_existing(rel)
207207
return dest
208208

209+
def integration_scripts_dir(self) -> Path | None:
210+
"""Return path to this integration's bundled ``scripts/`` directory.
211+
212+
Looks for a ``scripts/`` sibling of the module that defines the
213+
concrete subclass (not ``IntegrationBase`` itself).
214+
Returns ``None`` if the directory doesn't exist.
215+
"""
216+
import inspect
217+
218+
cls_file = inspect.getfile(type(self))
219+
scripts = Path(cls_file).resolve().parent / "scripts"
220+
return scripts if scripts.is_dir() else None
221+
222+
def install_scripts(
223+
self,
224+
project_root: Path,
225+
manifest: IntegrationManifest,
226+
) -> list[Path]:
227+
"""Copy integration-specific scripts into the project.
228+
229+
Copies files from this integration's ``scripts/`` directory to
230+
``.specify/integrations/<key>/scripts/`` in the project. Shell
231+
scripts are made executable. All copied files are recorded in
232+
*manifest*.
233+
234+
Returns the list of files created.
235+
"""
236+
scripts_src = self.integration_scripts_dir()
237+
if not scripts_src:
238+
return []
239+
240+
created: list[Path] = []
241+
scripts_dest = project_root / ".specify" / "integrations" / self.key / "scripts"
242+
scripts_dest.mkdir(parents=True, exist_ok=True)
243+
244+
for src_script in sorted(scripts_src.iterdir()):
245+
if not src_script.is_file():
246+
continue
247+
dst_script = scripts_dest / src_script.name
248+
shutil.copy2(src_script, dst_script)
249+
if dst_script.suffix == ".sh":
250+
dst_script.chmod(dst_script.stat().st_mode | 0o111)
251+
self.record_file_in_manifest(dst_script, project_root, manifest)
252+
created.append(dst_script)
253+
254+
return created
255+
209256
@staticmethod
210257
def process_template(
211258
content: str,
@@ -405,11 +452,51 @@ class MarkdownIntegration(IntegrationBase):
405452
Subclasses only need to set ``key``, ``config``, ``registrar_config``
406453
(and optionally ``context_file``). Everything else is inherited.
407454
408-
The default ``setup()`` from ``IntegrationBase`` copies templates
409-
into the agent's commands directory — which is correct for the
410-
standard Markdown case.
455+
``setup()`` processes command templates (replacing ``{SCRIPT}``,
456+
``{ARGS}``, ``__AGENT__``, rewriting paths) and installs
457+
integration-specific scripts (``update-context.sh`` / ``.ps1``).
411458
"""
412459

413-
# MarkdownIntegration inherits IntegrationBase.setup() as-is.
414-
# Future stages may add markdown-specific path rewriting here.
415-
pass
460+
def setup(
461+
self,
462+
project_root: Path,
463+
manifest: IntegrationManifest,
464+
parsed_options: dict[str, Any] | None = None,
465+
**opts: Any,
466+
) -> list[Path]:
467+
templates = self.list_command_templates()
468+
if not templates:
469+
return []
470+
471+
project_root_resolved = project_root.resolve()
472+
if manifest.project_root != project_root_resolved:
473+
raise ValueError(
474+
f"manifest.project_root ({manifest.project_root}) does not match "
475+
f"project_root ({project_root_resolved})"
476+
)
477+
478+
dest = self.commands_dest(project_root).resolve()
479+
try:
480+
dest.relative_to(project_root_resolved)
481+
except ValueError as exc:
482+
raise ValueError(
483+
f"Integration destination {dest} escapes "
484+
f"project root {project_root_resolved}"
485+
) from exc
486+
dest.mkdir(parents=True, exist_ok=True)
487+
488+
script_type = opts.get("script_type", "sh")
489+
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS") if self.registrar_config else "$ARGUMENTS"
490+
created: list[Path] = []
491+
492+
for src_file in templates:
493+
raw = src_file.read_text(encoding="utf-8")
494+
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
495+
dst_name = self.command_filename(src_file.stem)
496+
dst_file = self.write_file_and_record(
497+
processed, dest / dst_name, project_root, manifest
498+
)
499+
created.append(dst_file)
500+
501+
created.extend(self.install_scripts(project_root, manifest))
502+
return created

0 commit comments

Comments
 (0)