mirror of
https://github.com/github/spec-kit.git
synced 2026-08-15 14:02:04 +02:00
feat: allow overriding default init integration via SPECKIT_INTEGRATION_DEFAULT (#3952)
* feat: allow overriding default init integration via SPECKIT_INTEGRATION_DEFAULT Resolve the non-interactive/init default integration from the SPECKIT_INTEGRATION_DEFAULT environment variable, fitting the existing SPECKIT_INTEGRATION_* namespace. Falls back to the hardcoded "copilot" default when unset, and warns to stderr (rather than silently falling back) when the value is not a registered integration key. Wires the resolver into specify init (interactive prompt default and non-interactive fallback), the init workflow step, and the bundle init default. Adds unit and CLI tests and documents the variable. Closes #3939 Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: eecda55f-fa13-42f7-99bf-bfb0bb8565a0 * test: cover env-var default wiring for picker, workflow step, and bundle Address PR review: add regression tests so each SPECKIT_INTEGRATION_DEFAULT wiring site cannot silently revert to the hardcoded constant. - init.py: interactive picker receives the resolved key as default_key. - workflow init step: no step/workflow default + env var drives output integration and argv. - bundle _resolve_init_integration: env-var default applies when unspecified, while explicit override and manifest-declared integration still win. Assisted-by: GitHub Copilot (model: claude-opus-4.8, autonomous) Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: eecda55f-fa13-42f7-99bf-bfb0bb8565a0 --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: eecda55f-fa13-42f7-99bf-bfb0bb8565a0
This commit is contained in:
co-authored by
Copilot App
parent
4751777a38
commit
e9ffc9d8e7
@@ -77,7 +77,9 @@ from ._version import (
|
||||
from ._agent_config import (
|
||||
AGENT_CONFIG as AGENT_CONFIG,
|
||||
DEFAULT_INIT_INTEGRATION as DEFAULT_INIT_INTEGRATION,
|
||||
DEFAULT_INIT_INTEGRATION_ENV_VAR as DEFAULT_INIT_INTEGRATION_ENV_VAR,
|
||||
SCRIPT_TYPE_CHOICES as SCRIPT_TYPE_CHOICES,
|
||||
resolve_default_init_integration as resolve_default_init_integration,
|
||||
)
|
||||
from ._init_options import (
|
||||
INIT_OPTIONS_FILE as INIT_OPTIONS_FILE,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"""Agent configuration constants derived from the integration registry."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
|
||||
@@ -17,6 +19,36 @@ AGENT_CONFIG: dict[str, dict[str, Any]] = _build_agent_config()
|
||||
|
||||
DEFAULT_INIT_INTEGRATION = "copilot"
|
||||
|
||||
#: Environment variable used to override the fallback integration that
|
||||
#: ``specify init`` selects in non-interactive sessions. Follows the existing
|
||||
#: ``SPECKIT_INTEGRATION_*`` namespace (see ``SPECKIT_INTEGRATION_<KEY>_EXECUTABLE``
|
||||
#: and ``SPECKIT_INTEGRATION_CATALOG_URL``).
|
||||
DEFAULT_INIT_INTEGRATION_ENV_VAR = "SPECKIT_INTEGRATION_DEFAULT"
|
||||
|
||||
|
||||
def resolve_default_init_integration() -> str:
|
||||
"""Return the default init integration, honoring an env-var override.
|
||||
|
||||
Reads :data:`DEFAULT_INIT_INTEGRATION_ENV_VAR`
|
||||
(``SPECKIT_INTEGRATION_DEFAULT``). When it names a registered integration
|
||||
key, that key is returned; otherwise the hardcoded
|
||||
:data:`DEFAULT_INIT_INTEGRATION` (``"copilot"``) is used. An invalid value
|
||||
emits a warning to stderr rather than silently falling back, so operators
|
||||
can tell a typo from an intentional default.
|
||||
"""
|
||||
override = (os.environ.get(DEFAULT_INIT_INTEGRATION_ENV_VAR) or "").strip()
|
||||
if not override:
|
||||
return DEFAULT_INIT_INTEGRATION
|
||||
if override in AGENT_CONFIG:
|
||||
return override
|
||||
print(
|
||||
f"Warning: {DEFAULT_INIT_INTEGRATION_ENV_VAR}='{override}' is not a "
|
||||
f"recognized integration; falling back to '{DEFAULT_INIT_INTEGRATION}'. "
|
||||
f"Choose from: {', '.join(sorted(AGENT_CONFIG.keys()))}.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return DEFAULT_INIT_INTEGRATION
|
||||
|
||||
SCRIPT_TYPE_CHOICES: dict[str, str] = {
|
||||
"sh": "POSIX Shell (bash/zsh)",
|
||||
"ps": "PowerShell",
|
||||
|
||||
@@ -131,13 +131,13 @@ def _run_init(integration: str, *, script_type: str, offline: bool = False) -> N
|
||||
|
||||
def _resolve_init_integration(override: str | None, manifest) -> str:
|
||||
"""Precedence (FR-013): explicit override → bundle-declared → default."""
|
||||
from ..._agent_config import DEFAULT_INIT_INTEGRATION
|
||||
from ..._agent_config import resolve_default_init_integration
|
||||
|
||||
if override:
|
||||
return override
|
||||
if manifest is not None and manifest.integration is not None:
|
||||
return manifest.integration.id
|
||||
return DEFAULT_INIT_INTEGRATION
|
||||
return resolve_default_init_integration()
|
||||
|
||||
|
||||
# ===== Consume =====
|
||||
|
||||
@@ -14,8 +14,8 @@ from rich.panel import Panel
|
||||
|
||||
from .._agent_config import (
|
||||
AGENT_CONFIG,
|
||||
DEFAULT_INIT_INTEGRATION,
|
||||
SCRIPT_TYPE_CHOICES,
|
||||
resolve_default_init_integration,
|
||||
)
|
||||
from .._assets import (
|
||||
_locate_bundled_preset,
|
||||
@@ -466,17 +466,18 @@ def register(app: typer.Typer) -> None:
|
||||
raise typer.Exit(1)
|
||||
selected_ai = integration
|
||||
elif not _stdin_is_interactive():
|
||||
default_integration = resolve_default_init_integration()
|
||||
console.print(
|
||||
f"[dim]Non-interactive session detected: defaulting to '{DEFAULT_INIT_INTEGRATION}'. "
|
||||
f"[dim]Non-interactive session detected: defaulting to '{default_integration}'. "
|
||||
"Use --integration to choose a different agent.[/dim]"
|
||||
)
|
||||
selected_ai = DEFAULT_INIT_INTEGRATION
|
||||
selected_ai = default_integration
|
||||
else:
|
||||
ai_choices = {key: config["name"] for key, config in AGENT_CONFIG.items()}
|
||||
selected_ai = select_with_arrows(
|
||||
ai_choices,
|
||||
"Choose your coding agent integration:",
|
||||
DEFAULT_INIT_INTEGRATION,
|
||||
resolve_default_init_integration(),
|
||||
)
|
||||
|
||||
if not integration:
|
||||
|
||||
@@ -11,7 +11,10 @@ from __future__ import annotations
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from specify_cli._agent_config import DEFAULT_INIT_INTEGRATION, SCRIPT_TYPE_CHOICES
|
||||
from specify_cli._agent_config import (
|
||||
SCRIPT_TYPE_CHOICES,
|
||||
resolve_default_init_integration,
|
||||
)
|
||||
from specify_cli.workflows.base import StepBase, StepContext, StepResult, StepStatus
|
||||
from specify_cli.workflows.expressions import evaluate_expression
|
||||
|
||||
@@ -54,7 +57,8 @@ class InitStep(StepBase):
|
||||
Initialize in the target directory instead of creating a new one.
|
||||
``integration``
|
||||
Integration key (e.g. ``copilot``). Defaults to the workflow's
|
||||
default integration, then to ``DEFAULT_INIT_INTEGRATION``.
|
||||
default integration, then to the resolved default init integration
|
||||
(``SPECKIT_INTEGRATION_DEFAULT`` env var, else ``copilot``).
|
||||
``integration_options``
|
||||
Extra options for the integration (e.g. ``"--skills"`` or
|
||||
``"--commands-dir .myagent/cmds"``).
|
||||
@@ -81,7 +85,7 @@ class InitStep(StepBase):
|
||||
# Apply the same default that specify init uses in non-interactive mode
|
||||
# so that output.integration reflects the actual integration used.
|
||||
if not integration:
|
||||
integration = DEFAULT_INIT_INTEGRATION
|
||||
integration = resolve_default_init_integration()
|
||||
|
||||
integration_options = self._resolve(
|
||||
config.get("integration_options"), context
|
||||
|
||||
Reference in New Issue
Block a user