diff --git a/scripts/lib/__pycache__/common.cpython-312.pyc b/scripts/lib/__pycache__/common.cpython-312.pyc new file mode 100644 index 0000000..849226b Binary files /dev/null and b/scripts/lib/__pycache__/common.cpython-312.pyc differ diff --git a/scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc b/scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc new file mode 100644 index 0000000..0f80db2 Binary files /dev/null and b/scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc differ diff --git a/scripts/lib/common.py b/scripts/lib/common.py new file mode 100755 index 0000000..17250c4 --- /dev/null +++ b/scripts/lib/common.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python3 +""" +Common utilities for Moko-Cassiopeia scripts. + +Copyright (C) 2025 Moko Consulting + +This file is part of a Moko Consulting project. + +SPDX-License-Identifier: GPL-3.0-or-later + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program (./LICENSE.md). + +FILE INFORMATION +DEFGROUP: Script.Library +INGROUP: Common +REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +PATH: /scripts/lib/common.py +VERSION: 01.00.00 +BRIEF: Unified shared Python utilities for all CI and local scripts +""" + +import json +import os +import shutil +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional, Union +import subprocess +import traceback + + +# ============================================================================ +# Environment and Detection +# ============================================================================ + +def is_ci() -> bool: + """Check if running in CI environment.""" + return os.environ.get("CI", "").lower() == "true" + + +def require_cmd(command: str) -> None: + """ + Ensure a required command is available. + + Args: + command: Command name to check + + Raises: + SystemExit: If command is not found + """ + if not shutil.which(command): + log_error(f"Required command not found: {command}") + sys.exit(1) + + +# ============================================================================ +# Logging +# ============================================================================ + +class Colors: + """ANSI color codes for terminal output.""" + RED = '\033[0;31m' + GREEN = '\033[0;32m' + YELLOW = '\033[1;33m' + BLUE = '\033[0;34m' + CYAN = '\033[0;36m' + BOLD = '\033[1m' + NC = '\033[0m' # No Color + + @classmethod + def enabled(cls) -> bool: + """Check if colors should be enabled.""" + return sys.stdout.isatty() and os.environ.get("NO_COLOR") is None + + +def log_info(message: str) -> None: + """Log informational message.""" + print(f"INFO: {message}") + + +def log_warn(message: str) -> None: + """Log warning message.""" + color = Colors.YELLOW if Colors.enabled() else "" + nc = Colors.NC if Colors.enabled() else "" + print(f"{color}WARN: {message}{nc}", file=sys.stderr) + + +def log_error(message: str) -> None: + """Log error message.""" + color = Colors.RED if Colors.enabled() else "" + nc = Colors.NC if Colors.enabled() else "" + print(f"{color}ERROR: {message}{nc}", file=sys.stderr) + + +def log_success(message: str) -> None: + """Log success message.""" + color = Colors.GREEN if Colors.enabled() else "" + nc = Colors.NC if Colors.enabled() else "" + print(f"{color}✓ {message}{nc}") + + +def log_step(message: str) -> None: + """Log a step in a process.""" + color = Colors.CYAN if Colors.enabled() else "" + nc = Colors.NC if Colors.enabled() else "" + print(f"{color}➜ {message}{nc}") + + +def log_section(title: str) -> None: + """Log a section header.""" + print() + print("=" * 60) + print(title) + print("=" * 60) + + +def log_kv(key: str, value: str) -> None: + """Log a key-value pair.""" + print(f" {key}: {value}") + + +def die(message: str, exit_code: int = 1) -> None: + """ + Log error and exit. + + Args: + message: Error message + exit_code: Exit code (default: 1) + """ + log_error(message) + + if os.environ.get("VERBOSE_ERRORS", "true").lower() == "true": + print("", file=sys.stderr) + print("Stack trace:", file=sys.stderr) + traceback.print_stack(file=sys.stderr) + print("", file=sys.stderr) + print("Environment:", file=sys.stderr) + print(f" PWD: {os.getcwd()}", file=sys.stderr) + print(f" USER: {os.environ.get('USER', 'unknown')}", file=sys.stderr) + print(f" PYTHON: {sys.version}", file=sys.stderr) + print(f" CI: {is_ci()}", file=sys.stderr) + print("", file=sys.stderr) + + sys.exit(exit_code) + + +# ============================================================================ +# Validation Helpers +# ============================================================================ + +def assert_file_exists(path: Union[str, Path]) -> None: + """ + Assert that a file exists. + + Args: + path: Path to file + + Raises: + SystemExit: If file doesn't exist + """ + if not Path(path).is_file(): + die(f"Required file missing: {path}") + + +def assert_dir_exists(path: Union[str, Path]) -> None: + """ + Assert that a directory exists. + + Args: + path: Path to directory + + Raises: + SystemExit: If directory doesn't exist + """ + if not Path(path).is_dir(): + die(f"Required directory missing: {path}") + + +def assert_not_empty(value: Any, name: str) -> None: + """ + Assert that a value is not empty. + + Args: + value: Value to check + name: Name of the value for error message + + Raises: + SystemExit: If value is empty + """ + if not value: + die(f"Required value is empty: {name}") + + +# ============================================================================ +# JSON Utilities +# ============================================================================ + +def json_escape(text: str) -> str: + """ + Escape text for JSON. + + Args: + text: Text to escape + + Returns: + Escaped text + """ + return json.dumps(text)[1:-1] # Remove surrounding quotes + + +def json_output(data: Dict[str, Any], pretty: bool = False) -> None: + """ + Output data as JSON. + + Args: + data: Dictionary to output + pretty: Whether to pretty-print + """ + if pretty: + print(json.dumps(data, indent=2, sort_keys=True)) + else: + print(json.dumps(data, separators=(',', ':'))) + + +# ============================================================================ +# Path Utilities +# ============================================================================ + +def script_root() -> Path: + """ + Get the root scripts directory. + + Returns: + Path to scripts directory + """ + return Path(__file__).parent.parent + + +def repo_root() -> Path: + """ + Get the repository root directory. + + Returns: + Path to repository root + """ + return script_root().parent + + +def normalize_path(path: Union[str, Path]) -> str: + """ + Normalize a path (resolve, absolute, forward slashes). + + Args: + path: Path to normalize + + Returns: + Normalized path string + """ + return str(Path(path).resolve()).replace("\\", "/") + + +# ============================================================================ +# File Operations +# ============================================================================ + +def read_file(path: Union[str, Path], encoding: str = "utf-8") -> str: + """ + Read a file. + + Args: + path: Path to file + encoding: File encoding + + Returns: + File contents + """ + assert_file_exists(path) + return Path(path).read_text(encoding=encoding) + + +def write_file(path: Union[str, Path], content: str, encoding: str = "utf-8") -> None: + """ + Write a file. + + Args: + path: Path to file + content: Content to write + encoding: File encoding + """ + Path(path).write_text(content, encoding=encoding) + + +def ensure_dir(path: Union[str, Path]) -> None: + """ + Ensure a directory exists. + + Args: + path: Path to directory + """ + Path(path).mkdir(parents=True, exist_ok=True) + + +# ============================================================================ +# Command Execution +# ============================================================================ + +def run_command( + cmd: List[str], + capture_output: bool = True, + check: bool = True, + cwd: Optional[Union[str, Path]] = None, + env: Optional[Dict[str, str]] = None +) -> subprocess.CompletedProcess: + """ + Run a command. + + Args: + cmd: Command and arguments + capture_output: Whether to capture stdout/stderr + check: Whether to raise on non-zero exit + cwd: Working directory + env: Environment variables + + Returns: + CompletedProcess instance + """ + return subprocess.run( + cmd, + capture_output=capture_output, + text=True, + check=check, + cwd=cwd, + env=env + ) + + +def run_shell( + script: str, + capture_output: bool = True, + check: bool = True, + cwd: Optional[Union[str, Path]] = None +) -> subprocess.CompletedProcess: + """ + Run a shell script. + + Args: + script: Shell script + capture_output: Whether to capture stdout/stderr + check: Whether to raise on non-zero exit + cwd: Working directory + + Returns: + CompletedProcess instance + """ + return subprocess.run( + script, + shell=True, + capture_output=capture_output, + text=True, + check=check, + cwd=cwd + ) + + +# ============================================================================ +# Git Utilities +# ============================================================================ + +def git_root() -> Path: + """ + Get git repository root. + + Returns: + Path to git root + """ + result = run_command( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + check=True + ) + return Path(result.stdout.strip()) + + +def git_status(porcelain: bool = True) -> str: + """ + Get git status. + + Args: + porcelain: Use porcelain format + + Returns: + Git status output + """ + cmd = ["git", "status"] + if porcelain: + cmd.append("--porcelain") + + result = run_command(cmd, capture_output=True, check=True) + return result.stdout + + +def git_branch() -> str: + """ + Get current git branch. + + Returns: + Branch name + """ + result = run_command( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, + check=True + ) + return result.stdout.strip() + + +# ============================================================================ +# Main Entry Point (for testing) +# ============================================================================ + +def main() -> None: + """Test the common utilities.""" + log_section("Testing Common Utilities") + + log_info("This is an info message") + log_warn("This is a warning message") + log_success("This is a success message") + log_step("This is a step message") + + log_section("Environment") + log_kv("CI", str(is_ci())) + log_kv("Script Root", str(script_root())) + log_kv("Repo Root", str(repo_root())) + log_kv("Git Root", str(git_root())) + log_kv("Git Branch", git_branch()) + + log_section("Tests Passed") + + +if __name__ == "__main__": + main() diff --git a/scripts/lib/joomla_manifest.py b/scripts/lib/joomla_manifest.py new file mode 100755 index 0000000..c7322d1 --- /dev/null +++ b/scripts/lib/joomla_manifest.py @@ -0,0 +1,430 @@ +#!/usr/bin/env python3 +""" +Joomla manifest parsing and validation utilities. + +Copyright (C) 2025 Moko Consulting + +This file is part of a Moko Consulting project. + +SPDX-License-Identifier: GPL-3.0-or-later + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program (./LICENSE.md). + +FILE INFORMATION +DEFGROUP: Script.Library +INGROUP: Joomla.Manifest +REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +PATH: /scripts/lib/joomla_manifest.py +VERSION: 01.00.00 +BRIEF: Joomla manifest parsing and validation utilities +""" + +import xml.etree.ElementTree as ET +from pathlib import Path +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass + +try: + from . import common +except ImportError: + import common + + +# ============================================================================ +# Joomla Extension Types +# ============================================================================ + +class ExtensionType: + """Joomla extension types.""" + COMPONENT = "component" + MODULE = "module" + PLUGIN = "plugin" + TEMPLATE = "template" + LIBRARY = "library" + PACKAGE = "package" + FILE = "file" + LANGUAGE = "language" + + ALL_TYPES = [ + COMPONENT, MODULE, PLUGIN, TEMPLATE, + LIBRARY, PACKAGE, FILE, LANGUAGE + ] + + +# ============================================================================ +# Manifest Data Class +# ============================================================================ + +@dataclass +class ManifestInfo: + """Information extracted from a Joomla manifest.""" + path: Path + extension_type: str + name: str + version: str + description: Optional[str] = None + author: Optional[str] = None + author_email: Optional[str] = None + author_url: Optional[str] = None + copyright: Optional[str] = None + license: Optional[str] = None + creation_date: Optional[str] = None + + def to_dict(self) -> Dict[str, str]: + """Convert to dictionary.""" + return { + "path": str(self.path), + "ext_type": self.extension_type, + "name": self.name, + "version": self.version, + "description": self.description or "", + "author": self.author or "", + "author_email": self.author_email or "", + "author_url": self.author_url or "", + "copyright": self.copyright or "", + "license": self.license or "", + "creation_date": self.creation_date or "" + } + + +# ============================================================================ +# Manifest Discovery +# ============================================================================ + +def find_manifest(src_dir: str = "src") -> Path: + """ + Find the primary Joomla manifest in the given directory. + + Args: + src_dir: Source directory to search + + Returns: + Path to manifest file + + Raises: + SystemExit: If no manifest found + """ + src_path = Path(src_dir) + + if not src_path.is_dir(): + common.die(f"Source directory missing: {src_dir}") + + # Template manifest (templateDetails.xml) + template_manifest = src_path / "templateDetails.xml" + if template_manifest.is_file(): + return template_manifest + + # Also check in templates subdirectory + templates_dir = src_path / "templates" + if templates_dir.is_dir(): + for template_file in templates_dir.glob("templateDetails.xml"): + return template_file + + # Package manifest (pkg_*.xml) + pkg_manifests = list(src_path.rglob("pkg_*.xml")) + if pkg_manifests: + return pkg_manifests[0] + + # Component manifest (com_*.xml) + com_manifests = list(src_path.rglob("com_*.xml")) + if com_manifests: + return com_manifests[0] + + # Module manifest (mod_*.xml) + mod_manifests = list(src_path.rglob("mod_*.xml")) + if mod_manifests: + return mod_manifests[0] + + # Plugin manifest (plg_*.xml) + plg_manifests = list(src_path.rglob("plg_*.xml")) + if plg_manifests: + return plg_manifests[0] + + # Fallback: any XML with List[Path]: + """ + Find all Joomla manifests in the given directory. + + Args: + src_dir: Source directory to search + + Returns: + List of manifest paths + """ + src_path = Path(src_dir) + + if not src_path.is_dir(): + return [] + + manifests = [] + + # Template manifests + manifests.extend(src_path.rglob("templateDetails.xml")) + + # Package manifests + manifests.extend(src_path.rglob("pkg_*.xml")) + + # Component manifests + manifests.extend(src_path.rglob("com_*.xml")) + + # Module manifests + manifests.extend(src_path.rglob("mod_*.xml")) + + # Plugin manifests + manifests.extend(src_path.rglob("plg_*.xml")) + + return manifests + + +# ============================================================================ +# Manifest Parsing +# ============================================================================ + +def parse_manifest(manifest_path: Path) -> ManifestInfo: + """ + Parse a Joomla manifest file. + + Args: + manifest_path: Path to manifest file + + Returns: + ManifestInfo object + + Raises: + SystemExit: If parsing fails + """ + if not manifest_path.is_file(): + common.die(f"Manifest not found: {manifest_path}") + + try: + tree = ET.parse(manifest_path) + root = tree.getroot() + + # Extract extension type + ext_type = root.attrib.get("type", "").strip().lower() + if not ext_type: + common.die(f"Manifest missing type attribute: {manifest_path}") + + # Extract name + name_elem = root.find("name") + if name_elem is None or not name_elem.text: + common.die(f"Manifest missing name element: {manifest_path}") + name = name_elem.text.strip() + + # Extract version + version_elem = root.find("version") + if version_elem is None or not version_elem.text: + common.die(f"Manifest missing version element: {manifest_path}") + version = version_elem.text.strip() + + # Extract optional fields + description = None + desc_elem = root.find("description") + if desc_elem is not None and desc_elem.text: + description = desc_elem.text.strip() + + author = None + author_elem = root.find("author") + if author_elem is not None and author_elem.text: + author = author_elem.text.strip() + + author_email = None + email_elem = root.find("authorEmail") + if email_elem is not None and email_elem.text: + author_email = email_elem.text.strip() + + author_url = None + url_elem = root.find("authorUrl") + if url_elem is not None and url_elem.text: + author_url = url_elem.text.strip() + + copyright_text = None + copyright_elem = root.find("copyright") + if copyright_elem is not None and copyright_elem.text: + copyright_text = copyright_elem.text.strip() + + license_text = None + license_elem = root.find("license") + if license_elem is not None and license_elem.text: + license_text = license_elem.text.strip() + + creation_date = None + date_elem = root.find("creationDate") + if date_elem is not None and date_elem.text: + creation_date = date_elem.text.strip() + + return ManifestInfo( + path=manifest_path, + extension_type=ext_type, + name=name, + version=version, + description=description, + author=author, + author_email=author_email, + author_url=author_url, + copyright=copyright_text, + license=license_text, + creation_date=creation_date + ) + + except ET.ParseError as e: + common.die(f"Failed to parse manifest {manifest_path}: {e}") + except Exception as e: + common.die(f"Error reading manifest {manifest_path}: {e}") + + +def get_manifest_version(manifest_path: Path) -> str: + """ + Extract version from manifest. + + Args: + manifest_path: Path to manifest file + + Returns: + Version string + """ + info = parse_manifest(manifest_path) + return info.version + + +def get_manifest_name(manifest_path: Path) -> str: + """ + Extract name from manifest. + + Args: + manifest_path: Path to manifest file + + Returns: + Name string + """ + info = parse_manifest(manifest_path) + return info.name + + +def get_manifest_type(manifest_path: Path) -> str: + """ + Extract extension type from manifest. + + Args: + manifest_path: Path to manifest file + + Returns: + Extension type string + """ + info = parse_manifest(manifest_path) + return info.extension_type + + +# ============================================================================ +# Manifest Validation +# ============================================================================ + +def validate_manifest(manifest_path: Path) -> Tuple[bool, List[str]]: + """ + Validate a Joomla manifest. + + Args: + manifest_path: Path to manifest file + + Returns: + Tuple of (is_valid, list_of_warnings) + """ + warnings = [] + + try: + info = parse_manifest(manifest_path) + + # Check for recommended fields + if not info.description: + warnings.append("Missing description element") + + if not info.author: + warnings.append("Missing author element") + + if not info.copyright: + warnings.append("Missing copyright element") + + if not info.license: + warnings.append("Missing license element") + + if not info.creation_date: + warnings.append("Missing creationDate element") + + # Validate extension type + if info.extension_type not in ExtensionType.ALL_TYPES: + warnings.append(f"Unknown extension type: {info.extension_type}") + + return (True, warnings) + + except SystemExit: + return (False, ["Failed to parse manifest"]) + + +# ============================================================================ +# Main Entry Point (for testing) +# ============================================================================ + +def main() -> None: + """Test the manifest utilities.""" + import sys + + common.log_section("Testing Joomla Manifest Utilities") + + src_dir = sys.argv[1] if len(sys.argv) > 1 else "src" + + try: + manifest = find_manifest(src_dir) + common.log_success(f"Found manifest: {manifest}") + + info = parse_manifest(manifest) + + common.log_section("Manifest Information") + common.log_kv("Type", info.extension_type) + common.log_kv("Name", info.name) + common.log_kv("Version", info.version) + + if info.description: + common.log_kv("Description", info.description[:60] + "..." if len(info.description) > 60 else info.description) + + if info.author: + common.log_kv("Author", info.author) + + is_valid, warnings = validate_manifest(manifest) + + if is_valid: + common.log_success("Manifest is valid") + if warnings: + common.log_warn(f"Warnings: {len(warnings)}") + for warning in warnings: + print(f" - {warning}") + else: + common.log_error("Manifest validation failed") + + except SystemExit as e: + sys.exit(e.code) + + +if __name__ == "__main__": + main() diff --git a/scripts/run/scaffold_extension.py b/scripts/run/scaffold_extension.py new file mode 100755 index 0000000..19306f6 --- /dev/null +++ b/scripts/run/scaffold_extension.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +""" +Create Joomla extension scaffolding. + +Copyright (C) 2025 Moko Consulting + +This file is part of a Moko Consulting project. + +SPDX-License-Identifier: GPL-3.0-or-later + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program (./LICENSE.md). + +FILE INFORMATION +DEFGROUP: Moko-Cassiopeia.Scripts +INGROUP: Scripts.Run +REPO: https://github.com/mokoconsulting-tech/moko-cassiopeia +PATH: /scripts/run/scaffold_extension.py +VERSION: 01.00.00 +BRIEF: Create scaffolding for different Joomla extension types +""" + +import argparse +import sys +from datetime import datetime +from pathlib import Path +from typing import Dict + +# Add lib directory to path +sys.path.insert(0, str(Path(__file__).parent.parent / "lib")) + +try: + import common + import joomla_manifest +except ImportError: + print("ERROR: Cannot import required libraries", file=sys.stderr) + sys.exit(1) + + +# ============================================================================ +# Templates for Extension Scaffolding +# ============================================================================ + +def get_component_structure(name: str, description: str, author: str) -> Dict[str, str]: + """Get directory structure and files for a component.""" + safe_name = name.lower().replace(" ", "_") + com_name = f"com_{safe_name}" + + manifest = f""" + + {name} + {author} + {datetime.now().strftime("%Y-%m-%d")} + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + hello@example.com + https://example.com + 1.0.0 + {description} + + + src + + + + {name} + + services + sql + src + + + +""" + + return { + f"{com_name}.xml": manifest, + "site/src/.gitkeep": "", + "admin/services/provider.php": f" Dict[str, str]: + """Get directory structure and files for a module.""" + safe_name = name.lower().replace(" ", "_") + mod_name = f"mod_{safe_name}" + + manifest = f""" + + {name} + {author} + {datetime.now().strftime("%Y-%m-%d")} + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + hello@example.com + https://example.com + 1.0.0 + {description} + + + {mod_name}.php + {mod_name}.xml + tmpl + + +""" + + module_php = f"""get('layout', 'default')); +""" + + default_tmpl = f""" +
+

+
+""" + + return { + f"{mod_name}.xml": manifest, + f"{mod_name}.php": module_php, + "tmpl/default.php": default_tmpl, + } + + +def get_plugin_structure(name: str, description: str, author: str, group: str = "system") -> Dict[str, str]: + """Get directory structure and files for a plugin.""" + safe_name = name.lower().replace(" ", "_") + plg_name = f"{safe_name}" + + manifest = f""" + + plg_{group}_{safe_name} + {author} + {datetime.now().strftime("%Y-%m-%d")} + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + hello@example.com + https://example.com + 1.0.0 + {description} + + + {plg_name}.php + + +""" + + plugin_php = f""" Dict[str, str]: + """Get directory structure and files for a template.""" + safe_name = name.lower().replace(" ", "_") + + manifest = f""" + + {safe_name} + {datetime.now().strftime("%Y-%m-%d")} + {author} + hello@example.com + https://example.com + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + 1.0.0 + {description} + + + index.php + templateDetails.xml + css + js + images + + + + header + main + footer + + +""" + + index_php = f"""getDocument()->getWebAssetManager(); + +// Load template assets +$wa->useStyle('template.{safe_name}')->useScript('template.{safe_name}'); +?> + + + + + + + + +
+ +
+
+ +
+
+ +
+ + +""" + + return { + "templateDetails.xml": manifest, + "index.php": index_php, + "css/template.css": "/* Template styles */\n", + "js/template.js": "// Template JavaScript\n", + "images/.gitkeep": "", + } + + +def get_package_structure(name: str, description: str, author: str) -> Dict[str, str]: + """Get directory structure and files for a package.""" + safe_name = name.lower().replace(" ", "_") + pkg_name = f"pkg_{safe_name}" + + manifest = f""" + + {name} + {safe_name} + {author} + {datetime.now().strftime("%Y-%m-%d")} + Copyright (C) {datetime.now().year} {author} + GPL-3.0-or-later + hello@example.com + https://example.com + 1.0.0 + {description} + + + + + +""" + + return { + f"{pkg_name}.xml": manifest, + "packages/.gitkeep": "", + } + + +# ============================================================================ +# Scaffolding Functions +# ============================================================================ + +def create_extension( + ext_type: str, + name: str, + description: str, + author: str, + output_dir: str = "src", + **kwargs +) -> None: + """ + Create extension scaffolding. + + Args: + ext_type: Extension type (component, module, plugin, template, package) + name: Extension name + description: Extension description + author: Author name + output_dir: Output directory + **kwargs: Additional type-specific options + """ + output_path = Path(output_dir) + + # Get structure based on type + if ext_type == "component": + structure = get_component_structure(name, description, author) + elif ext_type == "module": + client = kwargs.get("client", "site") + structure = get_module_structure(name, description, author, client) + elif ext_type == "plugin": + group = kwargs.get("group", "system") + structure = get_plugin_structure(name, description, author, group) + elif ext_type == "template": + structure = get_template_structure(name, description, author) + elif ext_type == "package": + structure = get_package_structure(name, description, author) + else: + common.die(f"Unknown extension type: {ext_type}") + + # Create files + common.log_section(f"Creating {ext_type}: {name}") + + for file_path, content in structure.items(): + full_path = output_path / file_path + + # Create parent directories + full_path.parent.mkdir(parents=True, exist_ok=True) + + # Write file + full_path.write_text(content, encoding="utf-8") + common.log_success(f"Created: {file_path}") + + common.log_section("Scaffolding Complete") + common.log_info(f"Extension files created in: {output_path}") + common.log_info(f"Extension type: {ext_type}") + common.log_info(f"Extension name: {name}") + + +# ============================================================================ +# Command Line Interface +# ============================================================================ + +def main() -> None: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Create Joomla extension scaffolding", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Create a component + %(prog)s component MyComponent "My Component Description" "John Doe" + + # Create a module + %(prog)s module MyModule "My Module Description" "John Doe" --client site + + # Create a plugin + %(prog)s plugin MyPlugin "My Plugin Description" "John Doe" --group system + + # Create a template + %(prog)s template mytheme "My Theme Description" "John Doe" + + # Create a package + %(prog)s package mypackage "My Package Description" "John Doe" +""" + ) + + parser.add_argument( + "type", + choices=["component", "module", "plugin", "template", "package"], + help="Extension type to create" + ) + parser.add_argument("name", help="Extension name") + parser.add_argument("description", help="Extension description") + parser.add_argument("author", help="Author name") + parser.add_argument( + "-o", "--output", + default="src", + help="Output directory (default: src)" + ) + parser.add_argument( + "--client", + choices=["site", "administrator"], + default="site", + help="Module client (site or administrator)" + ) + parser.add_argument( + "--group", + default="system", + help="Plugin group (system, content, user, etc.)" + ) + + args = parser.parse_args() + + try: + create_extension( + ext_type=args.type, + name=args.name, + description=args.description, + author=args.author, + output_dir=args.output, + client=args.client, + group=args.group + ) + except Exception as e: + common.log_error(f"Failed to create extension: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main()