Skip to content

Commit 7f33dca

Browse files
darion-yaphetOmX
andauthored
refactor: create commands/ package and move init handler (PR-4/8) (#2615)
* refactor: create commands/ package and move init handler (PR-4/8) - Extract agent configuration constants (AGENT_CONFIG, AI_ASSISTANT_HELP, SCRIPT_TYPE_CHOICES, etc.) to _agent_config.py to avoid circular imports - Create commands/ package skeleton with stub modules for each command group - Move init command handler (~670 lines) from __init__.py to commands/init.py using the register(app) pattern; lazy imports inside the handler body prevent circular dependencies with __init__.py - Re-export AGENT_CONFIG, AI_ASSISTANT_HELP, SCRIPT_TYPE_CHOICES from __init__.py for backward compatibility - Add tests/test_commands_package.py to verify package structure * fix(tests): update patch targets after moving init handler to commands/init.py _stdin_is_interactive and select_with_arrows are now bound in specify_cli.commands.init, not specify_cli directly. * fix(lint): remove unused imports and mark re-exports in __init__.py - Remove shutil, shlex top-level imports (used lazily inside functions) - Remove rich.live.Live import (moved to commands/init.py) - Mark select_with_arrows and _locate_bundled_workflow as explicit re-exports to satisfy ruff F401 * chore: add from __future__ import annotations to new modules Aligns with the project convention established in _console.py, _assets.py, _utils.py, and other modules. * docs(cli): align init help with bundled scaffolding Potential fix for pull request finding Update command package documentation and init help text to reflect the current implementation: init uses bundled assets and integration setup, while placeholder command modules are import anchors until extracted. Remove the unused tracker-active flag assignment that had no reader in the codebase. Constraint: --offline is hidden/no-op and init no longer downloads templates from GitHub releases Rejected: Add no-op register functions to placeholder modules | would imply extracted command groups are implemented there Confidence: high Scope-risk: narrow Directive: Keep CLI help text aligned with the actual init scaffolding path Tested: uv run specify init --help; uv run pytest tests/test_commands_package.py tests/test_agent_config_consistency.py -q; uv run pytest tests/test_commands_package.py tests/test_console_imports.py tests/integrations/test_cli.py -q Not-tested: full test suite * fix(init): align preset failure reporting with _print_cli_warning helper Use the _print_cli_warning helper (introduced in main) for preset install failures so that output matches the expected format: "Failed to install preset '<name>': ..." "Continuing without the optional preset." * fix(init): remove unused lazy imports The init command imported CLI error formatting helpers through its circular-dependency-safe lazy import block, but the module does not use them. Remove those imports so ruff does not report F401. Constraint: uvx ruff check src/ must pass. Rejected: Wire the helpers into init error handling | Existing preset warnings already use _print_cli_warning, and changing behavior is unnecessary for this lint fix. Confidence: high Scope-risk: narrow Directive: Keep lazy import blocks limited to names consumed in the importing module. Tested: uvx ruff check src/ Not-tested: Full pytest suite Co-authored-by: OmX <omx@oh-my-codex.dev> --------- Co-authored-by: OmX <omx@oh-my-codex.dev>
1 parent e2ad589 commit 7f33dca

10 files changed

Lines changed: 866 additions & 804 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 13 additions & 802 deletions
Large diffs are not rendered by default.

src/specify_cli/_agent_config.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
"""Agent configuration constants derived from the integration registry."""
2+
from __future__ import annotations
3+
4+
from typing import Any
5+
6+
7+
def _build_agent_config() -> dict[str, dict[str, Any]]:
8+
from .integrations import INTEGRATION_REGISTRY
9+
config: dict[str, dict[str, Any]] = {}
10+
for key, integration in INTEGRATION_REGISTRY.items():
11+
if integration.config:
12+
config[key] = dict(integration.config)
13+
return config
14+
15+
16+
AGENT_CONFIG: dict[str, dict[str, Any]] = _build_agent_config()
17+
18+
DEFAULT_INIT_INTEGRATION = "copilot"
19+
20+
AI_ASSISTANT_ALIASES: dict[str, str] = {
21+
"kiro": "kiro-cli",
22+
}
23+
24+
25+
def _build_ai_assistant_help() -> str:
26+
non_generic_agents = sorted(agent for agent in AGENT_CONFIG if agent != "generic")
27+
base_help = (
28+
f"AI assistant to use: {', '.join(non_generic_agents)}, "
29+
"or generic (requires --ai-commands-dir)."
30+
)
31+
if not AI_ASSISTANT_ALIASES:
32+
return base_help
33+
alias_phrases = []
34+
for alias, target in sorted(AI_ASSISTANT_ALIASES.items()):
35+
alias_phrases.append(f"'{alias}' as an alias for '{target}'")
36+
if len(alias_phrases) == 1:
37+
aliases_text = alias_phrases[0]
38+
else:
39+
aliases_text = ", ".join(alias_phrases[:-1]) + " and " + alias_phrases[-1]
40+
return base_help + " Use " + aliases_text + "."
41+
42+
43+
AI_ASSISTANT_HELP: str = _build_ai_assistant_help()
44+
45+
SCRIPT_TYPE_CHOICES: dict[str, str] = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""CLI command groups extracted from the main application.
2+
3+
Implemented command modules expose a ``register(app)`` function. Placeholder
4+
modules are import-only anchors for command groups that still live in the main
5+
application module.
6+
"""
7+
from __future__ import annotations
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""specify extension * commands — placeholder for future extraction."""
2+
from __future__ import annotations

src/specify_cli/commands/init.py

Lines changed: 743 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""specify integration * commands — placeholder for future extraction."""
2+
from __future__ import annotations

src/specify_cli/commands/preset.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""specify preset * commands — placeholder for future extraction."""
2+
from __future__ import annotations
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""specify workflow * commands — placeholder for future extraction."""
2+
from __future__ import annotations

tests/integrations/test_integration_claude.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,8 @@ def test_interactive_claude_selection_uses_integration_path(self, tmp_path):
197197
os.chdir(project)
198198
runner = CliRunner()
199199
with (
200-
patch("specify_cli._stdin_is_interactive", return_value=True),
201-
patch("specify_cli.select_with_arrows", return_value="claude"),
200+
patch("specify_cli.commands.init._stdin_is_interactive", return_value=True),
201+
patch("specify_cli.commands.init.select_with_arrows", return_value="claude"),
202202
):
203203
result = runner.invoke(
204204
app,

tests/test_commands_package.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Tests for the commands/ package structure."""
2+
import importlib
3+
4+
5+
def test_commands_package_importable():
6+
mod = importlib.import_module("specify_cli.commands")
7+
assert mod is not None
8+
9+
10+
def test_commands_init_importable():
11+
mod = importlib.import_module("specify_cli.commands.init")
12+
assert hasattr(mod, "register")
13+
assert callable(mod.register)
14+
15+
16+
def test_commands_stubs_importable():
17+
for name in ("integration", "preset", "extension", "workflow"):
18+
mod = importlib.import_module(f"specify_cli.commands.{name}")
19+
assert mod is not None
20+
21+
22+
def test_agent_config_importable():
23+
from specify_cli._agent_config import (
24+
AGENT_CONFIG,
25+
AI_ASSISTANT_ALIASES,
26+
AI_ASSISTANT_HELP,
27+
DEFAULT_INIT_INTEGRATION,
28+
SCRIPT_TYPE_CHOICES,
29+
)
30+
assert isinstance(AGENT_CONFIG, dict)
31+
assert isinstance(AI_ASSISTANT_ALIASES, dict)
32+
assert isinstance(AI_ASSISTANT_HELP, str)
33+
assert DEFAULT_INIT_INTEGRATION == "copilot"
34+
assert "sh" in SCRIPT_TYPE_CHOICES
35+
36+
37+
def test_agent_config_re_exported_from_init():
38+
from specify_cli import AGENT_CONFIG, AI_ASSISTANT_ALIASES, AI_ASSISTANT_HELP, SCRIPT_TYPE_CHOICES
39+
assert isinstance(AGENT_CONFIG, dict)
40+
assert "sh" in SCRIPT_TYPE_CHOICES
41+
42+
43+
def test_init_command_registered():
44+
from specify_cli import app
45+
callback_names = [
46+
cmd.callback.__name__ for cmd in app.registered_commands if cmd.callback
47+
]
48+
assert "init" in callback_names

0 commit comments

Comments
 (0)