Skip to content
Merged
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
141 changes: 10 additions & 131 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,10 @@

import os
import sys
import json
from pathlib import Path

import typer
from rich.panel import Panel
from rich.align import Align
from rich.table import Table
from .shared_infra import (
install_shared_infra as _install_shared_infra_impl,
refresh_shared_templates as _refresh_shared_templates_impl,
Expand Down Expand Up @@ -389,137 +386,19 @@ def _print_cli_warning(
}


# ===== init command =====
# Moved to commands/init.py — registered here to preserve CLI surface.
from .commands import init as _init_cmd # noqa: E402
_init_cmd.register(app)
# ===== Root Commands =====

from . import command_check as _command_check # noqa: E402
from . import command_init as _command_init # noqa: E402
from . import command_version as _command_version # noqa: E402

@app.command()
def check():
"""Check that all required tools are installed."""
show_banner()
console.print("[bold]Checking for installed tools...[/bold]\n")
_command_init.register(app)
_command_check.register(app)
_command_version.register(app)

tracker = StepTracker("Check Available Tools")

agent_results = {}
for agent_key, agent_config in AGENT_CONFIG.items():
if agent_key == "generic":
continue # Generic is not a real agent to check
agent_name = agent_config["name"]
requires_cli = agent_config["requires_cli"]

tracker.add(agent_key, agent_name)

if requires_cli:
agent_results[agent_key] = check_tool(agent_key, tracker=tracker)
else:
# IDE-based agent - skip CLI check and mark as optional
tracker.skip(agent_key, "IDE-based, no CLI check")
agent_results[agent_key] = False # Don't count IDE agents as "found"

# Check VS Code variants (not in agent config)
tracker.add("code", "Visual Studio Code")
check_tool("code", tracker=tracker)

tracker.add("code-insiders", "Visual Studio Code Insiders")
check_tool("code-insiders", tracker=tracker)

console.print(tracker.render())

console.print("\n[bold green]Specify CLI is ready to use![/bold green]")

if not any(agent_results.values()):
console.print("[dim]Tip: Install a coding agent for the best experience[/dim]")

console.print("[dim]Tip: Run 'specify self check' to verify you have the latest CLI version[/dim]")


def _feature_capabilities() -> dict[str, bool]:
"""Return stable local CLI capability flags for humans and agents."""
return {
"controlled_multi_install_integrations": True,
"integration_use_command": True,
"multi_install_safe_registry_metadata": True,
"integration_upgrade_command": True,
"self_check_command": True,
"workflow_catalog": True,
"bundled_templates": True,
}


@app.command()
def version(
features: bool = typer.Option(
False,
"--features",
help="Show local CLI feature capabilities.",
),
json_output: bool = typer.Option(
False,
"--json",
help="Emit feature capabilities as JSON. Requires --features.",
),
):
"""Display version and system information."""
import platform

cli_version = get_speckit_version()

if json_output and not features:
console.print("[red]Error:[/red] --json requires --features.")
raise typer.Exit(1)

if features:
capabilities = _feature_capabilities()
if json_output:
payload = {"version": cli_version, "features": capabilities}
console.print(json.dumps(payload, indent=2))
return

console.print(f"Spec Kit CLI: {cli_version}")
console.print()
console.print("Features:")
for key, enabled in capabilities.items():
label = key.replace("_", " ")
console.print(f"- {label}: {'yes' if enabled else 'no'}")
return

show_banner()

info_table = Table(show_header=False, box=None, padding=(0, 2))
info_table.add_column("Key", style="cyan", justify="right")
info_table.add_column("Value", style="white")

info_table.add_row("CLI Version", cli_version)
info_table.add_row("", "")
info_table.add_row("Python", platform.python_version())
info_table.add_row("Platform", platform.system())
info_table.add_row("Architecture", platform.machine())
info_table.add_row("OS Version", platform.version())
# The OpenSSL runtime the interpreter actually loaded. HTTPS failure
# reports (#4433) hinge on which OpenSSL is in play, and on Windows it is
# not obvious from the outside, so surface it here. An interpreter built
# without the ssl extension skips the row rather than failing the command.
try:
import ssl

openssl_version = getattr(ssl, "OPENSSL_VERSION", "")
except ImportError:
openssl_version = ""
if openssl_version:
info_table.add_row("OpenSSL", openssl_version)

panel = Panel(
info_table,
title="[bold cyan]Specify CLI Information[/bold cyan]",
border_style="cyan",
padding=(1, 2)
)

console.print(panel)
console.print()
# Preserve root imports for handlers that were previously defined here.
check = _command_check.check
version = _command_version.version

app.add_typer(_self_app, name="self")

Expand Down
55 changes: 55 additions & 0 deletions src/specify_cli/command_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
"""CLI adapter for ``specify check``."""

from __future__ import annotations

import typer

from ._agent_config import AGENT_CONFIG
from ._console import StepTracker, console, show_banner


def check() -> None:
"""Check that all required tools are installed."""
from . import check_tool

show_banner()
console.print("[bold]Checking for installed tools...[/bold]\n")

tracker = StepTracker("Check Available Tools")

agent_results = {}
for agent_key, agent_config in AGENT_CONFIG.items():
if agent_key == "generic":
continue
agent_name = agent_config["name"]
requires_cli = agent_config["requires_cli"]

tracker.add(agent_key, agent_name)

if requires_cli:
agent_results[agent_key] = check_tool(agent_key, tracker=tracker)
else:
tracker.skip(agent_key, "IDE-based, no CLI check")
agent_results[agent_key] = False

tracker.add("code", "Visual Studio Code")
check_tool("code", tracker=tracker)

tracker.add("code-insiders", "Visual Studio Code Insiders")
check_tool("code-insiders", tracker=tracker)

console.print(tracker.render())

console.print("\n[bold green]Specify CLI is ready to use![/bold green]")

if not any(agent_results.values()):
console.print("[dim]Tip: Install a coding agent for the best experience[/dim]")

console.print(
"[dim]Tip: Run 'specify self check' to verify you have the latest CLI version[/dim]"
)


def register(app: typer.Typer) -> None:
"""Register ``specify check`` on the root application."""
app.command()(check)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""specify init command."""
"""CLI adapter for ``specify init``."""

from __future__ import annotations

Expand All @@ -15,18 +15,18 @@
from rich.markup import escape as _escape_markup
from rich.panel import Panel

from .._agent_config import (
from ._agent_config import (
AGENT_CONFIG,
SCRIPT_TYPE_CHOICES,
resolve_default_init_integration,
)
from .._assets import (
from ._assets import (
_locate_bundled_preset,
_locate_bundled_workflow,
get_speckit_version,
)
from .._console import StepTracker, console, select_with_arrows, show_banner
from .._utils import check_tool
from ._console import StepTracker, console, select_with_arrows, show_banner
from ._utils import check_tool


def _stdin_is_interactive() -> bool:
Expand Down Expand Up @@ -108,9 +108,9 @@ def _install_extension_during_init(project_path: Path, ext_spec: str, speckit_ve
"""
from urllib.parse import urlparse

from .._assets import _locate_bundled_extension
from ..extensions import ExtensionCatalog, ExtensionError, ExtensionManager
from ..extensions._commands import (
from ._assets import _locate_bundled_extension
from .extensions import ExtensionCatalog, ExtensionError, ExtensionManager
from .extensions._commands import (
_resolve_catalog_extension,
install_extension_from_url,
)
Expand Down Expand Up @@ -164,7 +164,7 @@ def _install_extension_during_init(project_path: Path, ext_spec: str, speckit_ve
return f"{manifest.name} v{manifest.version} installed"

if ext_info.get("bundled") and not ext_info.get("download_url"):
from ..extensions import REINSTALL_COMMAND
from .extensions import REINSTALL_COMMAND

raise ValueError(
f"Extension '{resolved_id}' is bundled with spec-kit but not found in the installed package. "
Expand Down Expand Up @@ -219,7 +219,7 @@ def ensure_constitution_from_template(
constitution) can seed the memory file. When nothing overrides it, the
resolver falls through to the core template.
"""
from ..presets import _materialize_constitution_template
from .presets import _materialize_constitution_template

memory_constitution = project_path / ".specify" / "memory" / "constitution.md"

Expand Down Expand Up @@ -380,24 +380,24 @@ def init(
specify init my-project --extension https://example.com/extensions/my-ext.zip --trust-extension-urls # URL extension (non-interactive)
"""
# Lazy imports to avoid circular dependency — __init__.py imports this module
from .. import (
from . import (
_install_shared_infra_or_exit,
_print_cli_warning,
ensure_executable_scripts,
save_init_options,
)
from ..integration_runtime import (
from .integration_runtime import (
invoke_prefix_for_integration as _invoke_prefix_for_integration,
with_integration_setting as _with_integration_setting,
)
from ..integrations._commands import (
from .integrations._commands import (
_parse_integration_options,
_write_integration_json,
)

show_banner()

from ..integrations import INTEGRATION_REGISTRY, get_integration
from .integrations import INTEGRATION_REGISTRY, get_integration

if integration:
resolved_integration = get_integration(integration)
Expand Down Expand Up @@ -667,7 +667,7 @@ def init(
) as live:
tracker.attach_refresh(lambda: live.update(tracker.render()))
try:
from ..integrations.manifest import IntegrationManifest
from .integrations.manifest import IntegrationManifest

tracker.start("integration")
manifest = IntegrationManifest(
Expand All @@ -684,7 +684,7 @@ def init(
if extra:
integration_parsed_options.update(extra)

from ..events import resolve_events
from .events import resolve_events
events_map = resolve_events(
resolved_integration.key,
resolved_integration.config,
Expand All @@ -702,7 +702,7 @@ def init(
manifest.save()

if force:
from ..integrations._helpers import (
from .integrations._helpers import (
_register_extensions_for_agent,
_register_presets_for_agent,
)
Expand Down Expand Up @@ -769,8 +769,8 @@ def init(
try:
bundled_wf = _locate_bundled_workflow("speckit")
if bundled_wf:
from ..workflows.catalog import WorkflowRegistry
from ..workflows.engine import WorkflowDefinition
from .workflows.catalog import WorkflowRegistry
from .workflows.engine import WorkflowDefinition

wf_registry = WorkflowRegistry(project_path)
if wf_registry.is_installed("speckit"):
Expand Down Expand Up @@ -823,7 +823,7 @@ def init(

if preset:
try:
from ..presets import PresetCatalog, PresetError, PresetManager
from .presets import PresetCatalog, PresetError, PresetManager

preset_manager = PresetManager(project_path)
speckit_ver = get_speckit_version()
Expand All @@ -849,7 +849,7 @@ def init(
elif pack_info.get("bundled") and not pack_info.get(
"download_url"
):
from ..extensions import REINSTALL_COMMAND
from .extensions import REINSTALL_COMMAND

console.print(
f"[yellow]Warning:[/yellow] Preset '{preset}' is bundled with spec-kit "
Expand Down Expand Up @@ -895,7 +895,7 @@ def init(

# Install extensions specified via --extension
if extensions:
from ..extensions._commands import _refresh_events_and_warn
from .extensions._commands import _refresh_events_and_warn

speckit_ver = get_speckit_version()
any_extension_installed = False
Expand Down Expand Up @@ -1093,7 +1093,7 @@ def init(
step_num += 1
usage_label = "skills" if native_skill_mode else "slash commands"

from .._invocation_style import (
from ._invocation_style import (
is_dollar_skills_agent as _is_dollar_skills_agent,
is_slash_skills_agent as _is_slash_skills_agent,
)
Expand Down
Loading
Loading