Skip to content
10 changes: 5 additions & 5 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1974,7 +1974,7 @@ def init(
console.print("[yellow]Use:[/yellow] --integration for the new integration system, or --ai for the legacy path")
raise typer.Exit(1)

# Auto-promote: --ai copilot → integration path with a nudge
# Auto-promote: --ai <key> → integration path with a nudge (if registered)
use_integration = False
if integration:
from .integrations import INTEGRATION_REGISTRY, get_integration
Expand All @@ -1987,14 +1987,14 @@ def init(
use_integration = True
# Map integration key to the ai_assistant variable for downstream compatibility
ai_assistant = integration
elif ai_assistant == "copilot":
elif ai_assistant:
from .integrations import get_integration
resolved_integration = get_integration("copilot")
resolved_integration = get_integration(ai_assistant)
if resolved_integration:
use_integration = True
console.print(
"[dim]Tip: Use [bold]--integration copilot[/bold] instead of "
"--ai copilot. The --ai flag will be deprecated in a future release.[/dim]"
f"[dim]Tip: Use [bold]--integration {ai_assistant}[/bold] instead of "
f"--ai {ai_assistant}. The --ai flag will be deprecated in a future release.[/dim]"
)

if project_name == ".":
Expand Down
8 changes: 7 additions & 1 deletion src/specify_cli/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class CommandRegistrar:
"args": "$ARGUMENTS",
"extension": ".agent.md"
},
"cursor": {
"cursor-agent": {
"dir": ".cursor/commands",
"format": "markdown",
"args": "$ARGUMENTS",
Expand Down Expand Up @@ -162,6 +162,12 @@ class CommandRegistrar:
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
},
"vibe": {
"dir": ".vibe/prompts",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md"
}
}

Expand Down
48 changes: 48 additions & 0 deletions src/specify_cli/integrations/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,53 @@ def _register_builtins() -> None:

_register(CopilotIntegration())

# Stage 3 — standard markdown integrations
from .claude import ClaudeIntegration
from .qwen import QwenIntegration
from .opencode import OpencodeIntegration
from .junie import JunieIntegration
from .kilocode import KilocodeIntegration
from .auggie import AuggieIntegration
from .roo import RooIntegration
from .codebuddy import CodebuddyIntegration
from .qodercli import QodercliIntegration
from .amp import AmpIntegration
from .shai import ShaiIntegration
from .bob import BobIntegration
from .trae import TraeIntegration
from .pi import PiIntegration
from .iflow import IflowIntegration

_register(ClaudeIntegration())
_register(QwenIntegration())
_register(OpencodeIntegration())
_register(JunieIntegration())
_register(KilocodeIntegration())
_register(AuggieIntegration())
_register(RooIntegration())
_register(CodebuddyIntegration())
_register(QodercliIntegration())
_register(AmpIntegration())
_register(ShaiIntegration())
_register(BobIntegration())
_register(TraeIntegration())
_register(PiIntegration())
_register(IflowIntegration())

# Hyphenated package names — use importlib for kiro-cli and cursor-agent
import importlib

kiro_mod = importlib.import_module(".kiro-cli", __package__)
_register(kiro_mod.KiroCliIntegration())

from .windsurf import WindsurfIntegration
from .vibe import VibeIntegration

_register(WindsurfIntegration())
_register(VibeIntegration())

cursor_mod = importlib.import_module(".cursor-agent", __package__)
_register(cursor_mod.CursorAgentIntegration())


_register_builtins()
21 changes: 21 additions & 0 deletions src/specify_cli/integrations/amp/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Amp CLI integration."""

from ..base import MarkdownIntegration


class AmpIntegration(MarkdownIntegration):
key = "amp"
config = {
"name": "Amp",
"folder": ".agents/",
"commands_subdir": "commands",
"install_url": "https://ampcode.com/manual#install",
"requires_cli": True,
}
registrar_config = {
"dir": ".agents/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = "AGENTS.md"
13 changes: 13 additions & 0 deletions src/specify_cli/integrations/amp/scripts/update-context.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# update-context.ps1 — Amp integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.

$ErrorActionPreference = 'Stop'

$repoRoot = git rev-parse --show-toplevel 2>$null
if (-not $repoRoot) { $repoRoot = $PWD.Path }

& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType amp
13 changes: 13 additions & 0 deletions src/specify_cli/integrations/amp/scripts/update-context.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# update-context.sh — Amp integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.

set -euo pipefail

REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"

exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" amp
21 changes: 21 additions & 0 deletions src/specify_cli/integrations/auggie/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Auggie CLI integration."""

from ..base import MarkdownIntegration


class AuggieIntegration(MarkdownIntegration):
key = "auggie"
config = {
"name": "Auggie CLI",
"folder": ".augment/",
"commands_subdir": "commands",
"install_url": "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli",
"requires_cli": True,
}
registrar_config = {
"dir": ".augment/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = ".augment/rules/specify-rules.md"
13 changes: 13 additions & 0 deletions src/specify_cli/integrations/auggie/scripts/update-context.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# update-context.ps1 — Auggie CLI integration: create/update .augment/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.

$ErrorActionPreference = 'Stop'

$repoRoot = git rev-parse --show-toplevel 2>$null
if (-not $repoRoot) { $repoRoot = $PWD.Path }

& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType auggie
13 changes: 13 additions & 0 deletions src/specify_cli/integrations/auggie/scripts/update-context.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# update-context.sh — Auggie CLI integration: create/update .augment/rules/specify-rules.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.

set -euo pipefail

REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"

exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" auggie
99 changes: 93 additions & 6 deletions src/specify_cli/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,53 @@ def write_file_and_record(
manifest.record_existing(rel)
return dest

def integration_scripts_dir(self) -> Path | None:
"""Return path to this integration's bundled ``scripts/`` directory.

Looks for a ``scripts/`` sibling of the module that defines the
concrete subclass (not ``IntegrationBase`` itself).
Returns ``None`` if the directory doesn't exist.
"""
import inspect

cls_file = inspect.getfile(type(self))
scripts = Path(cls_file).resolve().parent / "scripts"
return scripts if scripts.is_dir() else None

def install_scripts(
self,
project_root: Path,
manifest: IntegrationManifest,
) -> list[Path]:
"""Copy integration-specific scripts into the project.

Copies files from this integration's ``scripts/`` directory to
``.specify/integrations/<key>/scripts/`` in the project. Shell
scripts are made executable. All copied files are recorded in
*manifest*.

Returns the list of files created.
"""
scripts_src = self.integration_scripts_dir()
if not scripts_src:
return []

created: list[Path] = []
scripts_dest = project_root / ".specify" / "integrations" / self.key / "scripts"
scripts_dest.mkdir(parents=True, exist_ok=True)

for src_script in sorted(scripts_src.iterdir()):
if not src_script.is_file():
continue
dst_script = scripts_dest / src_script.name
shutil.copy2(src_script, dst_script)
if dst_script.suffix == ".sh":
dst_script.chmod(dst_script.stat().st_mode | 0o111)
self.record_file_in_manifest(dst_script, project_root, manifest)
created.append(dst_script)

return created

@staticmethod
def process_template(
content: str,
Expand Down Expand Up @@ -405,11 +452,51 @@ class MarkdownIntegration(IntegrationBase):
Subclasses only need to set ``key``, ``config``, ``registrar_config``
(and optionally ``context_file``). Everything else is inherited.

The default ``setup()`` from ``IntegrationBase`` copies templates
into the agent's commands directory — which is correct for the
standard Markdown case.
``setup()`` processes command templates (replacing ``{SCRIPT}``,
``{ARGS}``, ``__AGENT__``, rewriting paths) and installs
integration-specific scripts (``update-context.sh`` / ``.ps1``).
"""

# MarkdownIntegration inherits IntegrationBase.setup() as-is.
# Future stages may add markdown-specific path rewriting here.
pass
def setup(
self,
project_root: Path,
manifest: IntegrationManifest,
parsed_options: dict[str, Any] | None = None,
**opts: Any,
) -> list[Path]:
templates = self.list_command_templates()
if not templates:
return []

project_root_resolved = project_root.resolve()
if manifest.project_root != project_root_resolved:
raise ValueError(
f"manifest.project_root ({manifest.project_root}) does not match "
f"project_root ({project_root_resolved})"
)

dest = self.commands_dest(project_root).resolve()
try:
dest.relative_to(project_root_resolved)
except ValueError as exc:
raise ValueError(
f"Integration destination {dest} escapes "
f"project root {project_root_resolved}"
) from exc
dest.mkdir(parents=True, exist_ok=True)

script_type = opts.get("script_type", "sh")
arg_placeholder = self.registrar_config.get("args", "$ARGUMENTS") if self.registrar_config else "$ARGUMENTS"
created: list[Path] = []

for src_file in templates:
raw = src_file.read_text(encoding="utf-8")
processed = self.process_template(raw, self.key, script_type, arg_placeholder)
dst_name = self.command_filename(src_file.stem)
dst_file = self.write_file_and_record(
processed, dest / dst_name, project_root, manifest
)
created.append(dst_file)

created.extend(self.install_scripts(project_root, manifest))
return created
21 changes: 21 additions & 0 deletions src/specify_cli/integrations/bob/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""IBM Bob integration."""

from ..base import MarkdownIntegration


class BobIntegration(MarkdownIntegration):
key = "bob"
config = {
"name": "IBM Bob",
"folder": ".bob/",
"commands_subdir": "commands",
"install_url": None,
"requires_cli": False,
}
registrar_config = {
"dir": ".bob/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = "AGENTS.md"
13 changes: 13 additions & 0 deletions src/specify_cli/integrations/bob/scripts/update-context.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# update-context.ps1 — IBM Bob integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.

$ErrorActionPreference = 'Stop'

$repoRoot = git rev-parse --show-toplevel 2>$null
if (-not $repoRoot) { $repoRoot = $PWD.Path }

& "$repoRoot/.specify/scripts/powershell/update-agent-context.ps1" -AgentType bob
13 changes: 13 additions & 0 deletions src/specify_cli/integrations/bob/scripts/update-context.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# update-context.sh — IBM Bob integration: create/update AGENTS.md
#
# Thin wrapper that delegates to the shared update-agent-context script.
# Activated in Stage 7 when the shared script uses integration.json dispatch.
#
# Until then, this delegates to the shared script as a subprocess.

set -euo pipefail

REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"

exec "$REPO_ROOT/.specify/scripts/bash/update-agent-context.sh" bob
21 changes: 21 additions & 0 deletions src/specify_cli/integrations/claude/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Claude Code integration."""

from ..base import MarkdownIntegration


class ClaudeIntegration(MarkdownIntegration):
key = "claude"
config = {
"name": "Claude Code",
"folder": ".claude/",
"commands_subdir": "commands",
"install_url": "https://docs.anthropic.com/en/docs/claude-code/setup",
"requires_cli": True,
}
registrar_config = {
"dir": ".claude/commands",
"format": "markdown",
"args": "$ARGUMENTS",
"extension": ".md",
}
context_file = "CLAUDE.md"
Loading
Loading