class InitScaffoldTool:
"""Initialize a new Python project with best practices.
Registered as ``init_scaffold`` via axm.tools entry point.
"""
@property
def name(self) -> str:
"""Tool name used for MCP registration."""
return "init_scaffold"
def _validate_inputs(
self,
kwargs: dict[str, object],
) -> tuple[str, str | None, str, str, str, str, str, bool, str | None] | ToolResult:
"""Extract and validate inputs from kwargs.
Returns a tuple of validated values or a ToolResult on error.
"""
def _str(key: str, default: str = "") -> str:
v = kwargs.get(key, default)
return v if isinstance(v, str) else default
def _opt_str(key: str) -> str | None:
v = kwargs.get(key)
return v if isinstance(v, str) else None
path: str = _str("path", ".")
name: str | None = _opt_str("name")
org: str = _str("org")
author: str = _str("author")
email: str = _str("email")
license_type: str = _str("license", "Apache-2.0")
description: str = _str("description")
workspace_raw = kwargs.get("workspace", False)
workspace: bool = (
bool(workspace_raw) if isinstance(workspace_raw, bool) else False
)
member: str | None = _opt_str("member")
if not org or not author or not email:
return ToolResult(
success=False,
error="org, author, and email are required",
)
return (
path,
name,
org,
author,
email,
license_type,
description,
workspace,
member,
)
def _build_template_data(
self,
*,
project_name: str,
workspace: bool,
description: str,
meta: _ProjectMeta,
) -> dict[str, str]:
"""Build template data dict for workspace or standalone scaffold."""
name_key = "workspace_name" if workspace else "package_name"
default_desc = (
"A modern Python workspace" if workspace else "A modern Python package"
)
return {
name_key: project_name,
"description": description or default_desc,
"org": meta.org,
"license": meta.license_type,
"license_holder": meta.org,
"author_name": meta.author_name,
"author_email": meta.author_email,
}
def execute(self, **kwargs: object) -> ToolResult:
"""Initialize a new Python project.
Args:
**kwargs: Keyword arguments.
path: Path to initialize project.
name: Project name (defaults to directory name).
org: GitHub org or username.
author: Author name.
email: Author email.
license: License type.
description: Project description.
workspace: If True, scaffold a UV workspace.
member: Member package name to scaffold inside a workspace.
Returns:
ToolResult with created files list.
"""
validated = self._validate_inputs(kwargs)
if isinstance(validated, ToolResult):
return validated
path, name, org, author, email, license_type, description, workspace, member = (
validated
)
try:
target_path = Path(path).resolve()
if member:
return self._scaffold_member(
target_path,
member,
scaffold_data={
"org": org,
"author_name": author,
"author_email": email,
"license": license_type,
"description": description,
},
)
project_name = name or target_path.name
from axm_init.adapters.copier import CopierAdapter, CopierConfig
from axm_init.core.templates import TemplateType, get_template_path
template_type = (
TemplateType.WORKSPACE if workspace else TemplateType.STANDALONE
)
meta = _ProjectMeta(
org=org,
license_type=license_type,
author_name=author,
author_email=email,
)
data = self._build_template_data(
project_name=project_name,
workspace=workspace,
description=description,
meta=meta,
)
copier_adapter = CopierAdapter()
copier_config = CopierConfig(
template_path=get_template_path(template_type),
destination=target_path,
data=data,
trust_template=True,
)
result = copier_adapter.copy(copier_config)
return ToolResult(
success=result.success,
data={
"project_name": project_name,
"template": template_type.value,
"files": [str(f) for f in result.files_created],
},
error=None if result.success else result.message,
)
except Exception as exc:
return ToolResult(success=False, error=str(exc))
@staticmethod
def _resolve_workspace_root(target_path: Path) -> Path | None:
"""Resolve workspace root from target path, or None if not in a workspace."""
from axm_init.checks._workspace import (
ProjectContext,
detect_context,
find_workspace_root,
)
context = detect_context(target_path)
if context == ProjectContext.WORKSPACE:
return target_path
if context == ProjectContext.MEMBER:
return find_workspace_root(target_path)
return None
def _scaffold_member(
self,
target_path: Path,
member_name: str,
*,
scaffold_data: dict[str, str],
) -> ToolResult:
"""Scaffold a member sub-package inside an existing workspace.
Args:
target_path: Current directory (must be inside a workspace).
member_name: Name of the new member package.
scaffold_data: Template variables (org, author, email, etc.).
Returns:
ToolResult with member scaffold results.
"""
from axm_init.adapters.copier import CopierAdapter, CopierConfig
from axm_init.adapters.workspace_patcher import patch_all
from axm_init.core.templates import TemplateType, get_template_path
workspace_root = self._resolve_workspace_root(target_path)
if workspace_root is None:
return ToolResult(success=False, error="Not inside a UV workspace")
member_dir = workspace_root / "packages" / member_name
if member_dir.exists():
return ToolResult(
success=False,
error=f"Member '{member_name}' already exists at {member_dir}",
)
ws_name = read_workspace_name(workspace_root)
data = {
"member_name": member_name,
"workspace_name": ws_name,
**scaffold_data,
}
if "description" not in data or not data["description"]:
data["description"] = "A workspace member package"
copier_adapter = CopierAdapter()
copier_config = CopierConfig(
template_path=get_template_path(TemplateType.MEMBER),
destination=member_dir,
data=data,
trust_template=True,
)
result = copier_adapter.copy(copier_config)
if not result.success:
return ToolResult(
success=False,
error=result.message or "Member scaffold failed",
)
patched = patch_all(workspace_root, member_name)
return ToolResult(
success=True,
data={
"member": member_name,
"path": str(member_dir),
"files": [str(f) for f in result.files_created],
"patched_root_files": patched,
},
)