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:
BIN
scripts/lib/__pycache__/common.cpython-312.pyc
Normal file
BIN
scripts/lib/__pycache__/common.cpython-312.pyc
Normal file
Binary file not shown.
BIN
scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc
Normal file
BIN
scripts/lib/__pycache__/joomla_manifest.cpython-312.pyc
Normal file
Binary file not shown.
452
scripts/lib/common.py
Executable file
452
scripts/lib/common.py
Executable 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
430
scripts/lib/joomla_manifest.py
Executable 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
448
scripts/run/scaffold_extension.py
Executable 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()
|
||||||
Reference in New Issue
Block a user