feat: add Python library infrastructure and extension scaffolding

- Add scripts/lib/common.py with core utilities (logging, validation, JSON, file ops, git)
- Add scripts/lib/joomla_manifest.py for manifest parsing and validation
- Add scripts/run/scaffold_extension.py to create extension scaffolding
- Support all Joomla extension types (component, module, plugin, template, package)
- Maintain CLI compatibility with existing bash scripts
- Foundation for converting remaining scripts to Python

Co-authored-by: jmiller-moko <230051081+jmiller-moko@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-01-04 04:40:29 +00:00
parent 70f3a494ee
commit 4d8963fd27
5 changed files with 1330 additions and 0 deletions

Binary file not shown.

452
scripts/lib/common.py Executable file
View File

@@ -0,0 +1,452 @@
#!/usr/bin/env python3
"""
Common utilities for Moko-Cassiopeia scripts.
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
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()

430
scripts/lib/joomla_manifest.py Executable file
View File

@@ -0,0 +1,430 @@
#!/usr/bin/env python3
"""
Joomla manifest parsing and validation utilities.
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
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 <extension
for xml_file in src_path.rglob("*.xml"):
try:
content = xml_file.read_text(encoding="utf-8")
if "<extension" in content:
return xml_file
except Exception:
continue
common.die(f"No Joomla manifest XML found under {src_dir}")
def find_all_manifests(src_dir: str = "src") -> 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()

448
scripts/run/scaffold_extension.py Executable file
View File

@@ -0,0 +1,448 @@
#!/usr/bin/env python3
"""
Create Joomla extension scaffolding.
Copyright (C) 2025 Moko Consulting <hello@mokoconsulting.tech>
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"""<?xml version="1.0" encoding="utf-8"?>
<extension type="component" version="4.0" method="upgrade">
<name>{name}</name>
<author>{author}</author>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl>
<version>1.0.0</version>
<description>{description}</description>
<files folder="site">
<folder>src</folder>
</files>
<administration>
<menu>{name}</menu>
<files folder="admin">
<folder>services</folder>
<folder>sql</folder>
<folder>src</folder>
</files>
</administration>
</extension>
"""
return {
f"{com_name}.xml": manifest,
"site/src/.gitkeep": "",
"admin/services/provider.php": f"<?php\n// Service provider for {name}\n",
"admin/sql/install.mysql.utf8.sql": "-- Installation SQL\n",
"admin/sql/uninstall.mysql.utf8.sql": "-- Uninstallation SQL\n",
"admin/src/.gitkeep": "",
}
def get_module_structure(name: str, description: str, author: str, client: str = "site") -> Dict[str, str]:
"""Get directory structure and files for a module."""
safe_name = name.lower().replace(" ", "_")
mod_name = f"mod_{safe_name}"
manifest = f"""<?xml version="1.0" encoding="utf-8"?>
<extension type="module" version="4.0" client="{client}" method="upgrade">
<name>{name}</name>
<author>{author}</author>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl>
<version>1.0.0</version>
<description>{description}</description>
<files>
<filename module="{mod_name}">{mod_name}.php</filename>
<filename>{mod_name}.xml</filename>
<folder>tmpl</folder>
</files>
</extension>
"""
module_php = f"""<?php
/**
* @package {name}
* @copyright Copyright (C) {datetime.now().year} {author}
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
// Module logic here
require JModuleHelper::getLayoutPath('mod_{safe_name}', $params->get('layout', 'default'));
"""
default_tmpl = f"""<?php
/**
* @package {name}
* @copyright Copyright (C) {datetime.now().year} {author}
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
?>
<div class="{mod_name}">
<p><?php echo JText::_('MOD_{safe_name.upper()}_DESCRIPTION'); ?></p>
</div>
"""
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"""<?xml version="1.0" encoding="utf-8"?>
<extension type="plugin" version="4.0" group="{group}" method="upgrade">
<name>plg_{group}_{safe_name}</name>
<author>{author}</author>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl>
<version>1.0.0</version>
<description>{description}</description>
<files>
<filename plugin="{plg_name}">{plg_name}.php</filename>
</files>
</extension>
"""
plugin_php = f"""<?php
/**
* @package {name}
* @copyright Copyright (C) {datetime.now().year} {author}
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\\CMS\\Plugin\\CMSPlugin;
class Plg{group.capitalize()}{plg_name.capitalize()} extends CMSPlugin
{{
protected $autoloadLanguage = true;
public function onContentPrepare($context, &$article, &$params, $limitstart = 0)
{{
// Plugin logic here
}}
}}
"""
return {
f"plg_{group}_{safe_name}.xml": manifest,
f"{plg_name}.php": plugin_php,
}
def get_template_structure(name: str, description: str, author: str) -> Dict[str, str]:
"""Get directory structure and files for a template."""
safe_name = name.lower().replace(" ", "_")
manifest = f"""<?xml version="1.0" encoding="utf-8"?>
<extension type="template" version="4.0" client="site" method="upgrade">
<name>{safe_name}</name>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<author>{author}</author>
<authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license>
<version>1.0.0</version>
<description>{description}</description>
<files>
<filename>index.php</filename>
<filename>templateDetails.xml</filename>
<folder>css</folder>
<folder>js</folder>
<folder>images</folder>
</files>
<positions>
<position>header</position>
<position>main</position>
<position>footer</position>
</positions>
</extension>
"""
index_php = f"""<?php
/**
* @package {name}
* @copyright Copyright (C) {datetime.now().year} {author}
* @license GPL-3.0-or-later
*/
defined('_JEXEC') or die;
use Joomla\\CMS\\Factory;
use Joomla\\CMS\\HTML\\HTMLHelper;
use Joomla\\CMS\\Uri\\Uri;
$app = Factory::getApplication();
$wa = $app->getDocument()->getWebAssetManager();
// Load template assets
$wa->useStyle('template.{safe_name}')->useScript('template.{safe_name}');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
<jdoc:include type="metas" />
<jdoc:include type="styles" />
<jdoc:include type="scripts" />
</head>
<body>
<header>
<jdoc:include type="modules" name="header" style="html5" />
</header>
<main>
<jdoc:include type="component" />
</main>
<footer>
<jdoc:include type="modules" name="footer" style="html5" />
</footer>
</body>
</html>
"""
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"""<?xml version="1.0" encoding="utf-8"?>
<extension type="package" version="4.0" method="upgrade">
<name>{name}</name>
<packagename>{safe_name}</packagename>
<author>{author}</author>
<creationDate>{datetime.now().strftime("%Y-%m-%d")}</creationDate>
<copyright>Copyright (C) {datetime.now().year} {author}</copyright>
<license>GPL-3.0-or-later</license>
<authorEmail>hello@example.com</authorEmail>
<authorUrl>https://example.com</authorUrl>
<version>1.0.0</version>
<description>{description}</description>
<files folder="packages">
<!-- Add extension packages here -->
</files>
</extension>
"""
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()