Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/powershell/common.ps1
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env pwsh
# Common PowerShell functions analogous to common.sh


# Find repository root by searching upward for .specify directory
# This is the primary marker for spec-kit projects
function Find-SpecifyRoot {
Expand Down
21 changes: 17 additions & 4 deletions src/specify_cli/integrations/agy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,26 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
# agy does not support --model or JSON output; both params are ignored
self.validate_runtime_config(integration_args, integration_options)
args = [self._resolve_executable(), "--print", prompt]
# Honor SPECKIT_INTEGRATION_AGY_EXTRA_ARGS (operator-supplied flags),
# appended after the positional prompt like the devin integration.
# agy does not support JSON output; output_json is ignored.
args = [self._resolve_executable()]
# Pass --model before --print so agy can parse it as a flag.
# agy >=1.20 supports: agy --model <name> --print <prompt>
if model:
args.extend(["--model", model])
# Inject --add-dir so agy discovers the project workspace when invoked
# from an arbitrary working directory (e.g. the workflow engine's cwd).
# Without this agy falls back to its own scratch directory and cannot
# locate .agents/skills/, reporting "no active workspace".
if project_root is not None:
args.extend(["--add-dir", str(project_root.resolve())])
# Honor SPECKIT_INTEGRATION_AGY_EXTRA_ARGS (operator-supplied flags).
# These MUST be inserted before --print because agy treats every token
# that follows --print as part of the prompt, not as CLI flags.
self._apply_extra_args_env_var(args)
args.extend(["--print", prompt])
return args

def setup(
Expand Down
5 changes: 5 additions & 0 deletions src/specify_cli/integrations/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build CLI arguments for non-interactive execution.

Expand Down Expand Up @@ -410,6 +411,7 @@ def dispatch_command(
output_json=not stream,
integration_args=integration_args,
integration_options=integration_options,
project_root=project_root,
)

if exec_args is None:
Expand Down Expand Up @@ -1067,6 +1069,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
self.validate_runtime_config(integration_args, integration_options)
if not self.config or not self.config.get("requires_cli"):
Expand Down Expand Up @@ -1161,6 +1164,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
self.validate_runtime_config(integration_args, integration_options)
if not self.config or not self.config.get("requires_cli"):
Expand Down Expand Up @@ -1633,6 +1637,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
self.validate_runtime_config(integration_args, integration_options)
if not self.config or not self.config.get("requires_cli"):
Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/codex/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""

from __future__ import annotations
from pathlib import Path

from collections.abc import Mapping, Sequence
from typing import Any
Expand Down Expand Up @@ -51,6 +52,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
# Codex uses ``codex exec "prompt"`` for non-interactive mode.
# Resolve argv[0] via the shared executable resolver so operators can
Expand Down
1 change: 1 addition & 0 deletions src/specify_cli/integrations/copilot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
self.validate_runtime_config(integration_args, integration_options)
# GitHub Copilot CLI uses ``copilot -p "prompt"`` for
Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/cursor_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

from __future__ import annotations
from pathlib import Path

from collections.abc import Mapping, Sequence
from typing import Any
Expand Down Expand Up @@ -68,6 +69,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build CLI arguments for non-interactive ``cursor-agent`` execution.

Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/devin/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

from __future__ import annotations
from pathlib import Path

from collections.abc import Mapping, Sequence
from typing import Any
Expand Down Expand Up @@ -63,6 +64,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build non-interactive CLI args for Devin for Terminal.

Expand Down
3 changes: 3 additions & 0 deletions src/specify_cli/integrations/docker_agent/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

from __future__ import annotations
from pathlib import Path

import os
import shlex
Expand Down Expand Up @@ -85,6 +86,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build a headless Docker Agent invocation with an agent config."""
self.validate_runtime_config(integration_args, integration_options)
Expand Down Expand Up @@ -152,6 +154,7 @@ def validate_runtime_config(
self,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> None:
"""Validate Docker Agent's per-step agent reference and CLI options."""
runtime_args = list(integration_args or ())
Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/droid/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

from __future__ import annotations
from pathlib import Path

from collections.abc import Mapping, Sequence
from typing import Any
Expand Down Expand Up @@ -95,6 +96,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build CLI arguments for non-interactive ``droid`` execution.

Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/dsh/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"""

from __future__ import annotations
from pathlib import Path

from collections.abc import Mapping, Sequence
from typing import Any
Expand Down Expand Up @@ -51,6 +52,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build non-interactive CLI args for DSH.

Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/goose/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Goose integration — open source AI agent (Agentic AI Foundation)."""

from __future__ import annotations
from pathlib import Path

from collections.abc import Mapping, Sequence
from typing import Any
Expand Down Expand Up @@ -32,6 +33,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build CLI arguments for non-interactive ``goose`` execution.

Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/grok/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

from __future__ import annotations
from pathlib import Path

from collections.abc import Mapping, Sequence
from typing import Any
Expand Down Expand Up @@ -40,6 +41,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build CLI arguments for non-interactive ``grok`` execution.

Expand Down
1 change: 1 addition & 0 deletions src/specify_cli/integrations/hermes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build Hermes CLI invocation for programmatic dispatch.

Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/muse/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"""

from __future__ import annotations
from pathlib import Path

from collections.abc import Mapping, Sequence
from typing import Any
Expand Down Expand Up @@ -55,6 +56,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
# Muse Code uses ``muse exec "<prompt>"`` for non-interactive mode.
# Resolve argv[0] via the shared executable resolver so operators can
Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/omp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Oh My Pi (omp) coding agent integration."""

from __future__ import annotations
from pathlib import Path

from collections.abc import Mapping, Sequence
from typing import Any
Expand Down Expand Up @@ -33,6 +34,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
# Diverges from MarkdownIntegration.build_exec_args because OMP's
# CLI parser treats `-p`/`--print` as a boolean (one-shot mode) and
Expand Down
2 changes: 2 additions & 0 deletions src/specify_cli/integrations/opencode/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""opencode integration."""

from collections.abc import Mapping, Sequence
from pathlib import Path
from typing import Any

from ..base import MarkdownIntegration
Expand Down Expand Up @@ -48,6 +49,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
self.validate_runtime_config(integration_args, integration_options)
args = [self._resolve_executable(), "run"]
Expand Down
1 change: 1 addition & 0 deletions src/specify_cli/integrations/rovodev/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def build_exec_args(
output_json: bool = True,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
project_root: Path | None = None,
) -> list[str] | None:
"""Build non-interactive ACLI args for RovoDev.

Expand Down
5 changes: 3 additions & 2 deletions src/specify_cli/workflows/steps/command/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,14 @@ def _try_dispatch(

impl.validate_runtime_config(integration_args, integration_options)

project_root = Path(context.project_root) if context.project_root else None

# Build sample args for fallback executable detection when impl.key is not executable.
exec_args = impl.build_exec_args(
"test",
integration_args=integration_args,
integration_options=integration_options,
project_root=project_root,
)

# Check if the CLI tool is actually installed.
Expand All @@ -248,8 +251,6 @@ def _try_dispatch(
if cli_path is None and fallback_cli_path is None:
return None

project_root = Path(context.project_root) if context.project_root else None

try:
return impl.dispatch_command(
command,
Expand Down
15 changes: 10 additions & 5 deletions src/specify_cli/workflows/steps/prompt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,16 @@ def _try_dispatch(
if impl is None:
return None

exec_args = impl.build_exec_args(prompt, model=model, output_json=False)
project_root = (
Path(context.project_root) if context.project_root else Path.cwd()
)

exec_args = impl.build_exec_args(
prompt,
model=model,
output_json=False,
project_root=project_root,
)

# Check if the CLI tool is actually installed.
# Try the integration key first (covers most agents), then fall back
Expand All @@ -227,10 +236,6 @@ def _try_dispatch(

import subprocess

project_root = (
Path(context.project_root) if context.project_root else Path.cwd()
)

try:
result = subprocess.run(
exec_args,
Expand Down
5 changes: 5 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ def _strip_specify_env(monkeypatch):
that wants an override sets it explicitly via monkeypatch afterwards."""
for key in [k for k in os.environ if k.startswith("SPECIFY_")]:
monkeypatch.delenv(key, raising=False)
for key in list(os.environ):
if key.startswith("SPECKIT_INTEGRATION_") and (
key.endswith("_EXTRA_ARGS") or key.endswith("_EXECUTABLE")
):
monkeypatch.delenv(key, raising=False)


@pytest.fixture
Expand Down
1 change: 1 addition & 0 deletions tests/integrations/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def test_all_builtin_exec_builders_accept_runtime_config(self):
parameters = inspect.signature(integration.build_exec_args).parameters
assert "integration_args" in parameters, key
assert "integration_options" in parameters, key
assert "project_root" in parameters, key

def test_unsupported_exec_builders_reject_runtime_config_directly(self):
from specify_cli.integrations import INTEGRATION_REGISTRY
Expand Down
Loading