From efb3cfe08de09b9fb6375839f9ac6326c16a0bbe Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 19 Nov 2025 00:02:22 +0500 Subject: [PATCH 01/16] feat: Add installation templates system for common development stacks --- cortex/cli.py | 372 ++++++++++++++++++++++- cortex/templates.py | 518 +++++++++++++++++++++++++++++++ cortex/templates/README.md | 65 ++++ cortex/templates/devops.yaml | 93 ++++++ cortex/templates/lamp.yaml | 68 +++++ cortex/templates/mean.yaml | 64 ++++ cortex/templates/mern.yaml | 64 ++++ cortex/templates/ml-ai.yaml | 66 ++++ docs/TEMPLATES.md | 570 +++++++++++++++++++++++++++++++++++ src/requirements.txt | 1 + test/test_templates.py | 451 +++++++++++++++++++++++++++ 11 files changed, 2322 insertions(+), 10 deletions(-) create mode 100644 cortex/templates.py create mode 100644 cortex/templates/README.md create mode 100644 cortex/templates/devops.yaml create mode 100644 cortex/templates/lamp.yaml create mode 100644 cortex/templates/mean.yaml create mode 100644 cortex/templates/mern.yaml create mode 100644 cortex/templates/ml-ai.yaml create mode 100644 docs/TEMPLATES.md create mode 100644 test/test_templates.py diff --git a/cortex/cli.py b/cortex/cli.py index cdb60442..d9fe5bd5 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -10,6 +10,7 @@ from LLM.interpreter import CommandInterpreter from cortex.coordinator import InstallationCoordinator, StepStatus +from cortex.templates import TemplateManager, Template, TemplateFormat, InstallationStep from installation_history import ( InstallationHistory, InstallationType, @@ -55,19 +56,24 @@ def _clear_line(self): sys.stdout.write('\r\033[K') sys.stdout.flush() - def install(self, software: str, execute: bool = False, dry_run: bool = False): - api_key = self._get_api_key() - if not api_key: - return 1 - - provider = self._get_provider() - + def install(self, software: str, execute: bool = False, dry_run: bool = False, template: Optional[str] = None): # Initialize installation history history = InstallationHistory() install_id = None start_time = datetime.now() try: + # If template is specified, use template system + if template: + return self._install_from_template(template, execute, dry_run) + + # Otherwise, use LLM-based installation + api_key = self._get_api_key() + if not api_key: + return 1 + + provider = self._get_provider() + self._print_status("🧠", "Understanding request...") interpreter = CommandInterpreter(api_key=api_key, provider=provider) @@ -261,6 +267,306 @@ def rollback(self, install_id: str, dry_run: bool = False): except Exception as e: self._print_error(f"Rollback failed: {str(e)}") return 1 + + def _install_from_template(self, template_name: str, execute: bool, dry_run: bool): + """Install from a template.""" + history = InstallationHistory() + install_id = None + start_time = datetime.now() + + try: + template_manager = TemplateManager() + + self._print_status("[*]", f"Loading template: {template_name}...") + template = template_manager.load_template(template_name) + + if not template: + self._print_error(f"Template '{template_name}' not found") + self._print_status("[*]", "Available templates:") + templates = template_manager.list_templates() + for name, info in templates.items(): + print(f" - {name}: {info['description']}") + return 1 + + # Display template info + print(f"\n{template.name} Template:") + print(f" {template.description}") + print(f"\n Packages:") + for pkg in template.packages: + print(f" - {pkg}") + + # Check hardware compatibility + is_compatible, warnings = template_manager.check_hardware_compatibility(template) + if warnings: + print(f"\n[WARNING] Hardware Compatibility Warnings:") + for warning in warnings: + print(f" - {warning}") + if not is_compatible and not dry_run: + try: + response = input("\n[WARNING] Hardware requirements not met. Continue anyway? (y/N): ") + if response.lower() != 'y': + return 1 + except (EOFError, KeyboardInterrupt): + # Non-interactive environment or user cancelled + print("\n[INFO] Skipping hardware check prompt (non-interactive mode)") + return 1 + + # Generate commands + self._print_status("[*]", "Generating installation commands...") + commands = template_manager.generate_commands(template) + + if not commands: + self._print_error("No commands generated from template") + return 1 + + # Extract packages for tracking + packages = template.packages if template.packages else history._extract_packages_from_commands(commands) + + # Record installation start + if execute or dry_run: + install_id = history.record_installation( + InstallationType.INSTALL, + packages, + commands, + start_time + ) + + print(f"\n[*] Installing {len(packages)} packages...") + print("\nGenerated commands:") + for i, cmd in enumerate(commands, 1): + print(f" {i}. {cmd}") + + if dry_run: + print("\n(Dry run mode - commands not executed)") + if install_id: + history.update_installation(install_id, InstallationStatus.SUCCESS) + return 0 + + if execute: + # Convert template steps to coordinator format if available + if template.steps: + plan = [ + { + "command": step.command, + "description": step.description, + "rollback": step.rollback + } + for step in template.steps + ] + coordinator = InstallationCoordinator.from_plan( + plan, + timeout=300, + stop_on_error=True + ) + else: + def progress_callback(current, total, step): + status_emoji = "ā³" + if step.status == StepStatus.SUCCESS: + status_emoji = "āœ…" + elif step.status == StepStatus.FAILED: + status_emoji = "āŒ" + print(f"\n[{current}/{total}] {status_emoji} {step.description}") + print(f" Command: {step.command}") + + coordinator = InstallationCoordinator( + commands=commands, + descriptions=[f"Step {i+1}" for i in range(len(commands))], + timeout=300, + stop_on_error=True, + progress_callback=progress_callback + ) + + print("\nExecuting commands...") + result = coordinator.execute() + + if result.success: + # Run verification commands if available + if template.verification_commands: + self._print_status("[*]", "Verifying installation...") + verify_results = coordinator.verify_installation(template.verification_commands) + all_passed = all(verify_results.values()) + if not all_passed: + print("\n[WARNING] Some verification checks failed:") + for cmd, passed in verify_results.items(): + status = "[OK]" if passed else "[FAIL]" + print(f" {status} {cmd}") + + # Run post-install commands + if template.post_install: + self._print_status("[*]", "Running post-installation steps...") + for cmd in template.post_install: + subprocess.run(cmd, shell=True) + + self._print_success(f"{template.name} stack ready!") + print(f"\nCompleted in {result.total_duration:.2f} seconds") + + # Display post-install info + if template.post_install: + print("\n[*] Post-installation information:") + for cmd in template.post_install: + if cmd.startswith("echo"): + subprocess.run(cmd, shell=True) + + # Record successful installation + if install_id: + history.update_installation(install_id, InstallationStatus.SUCCESS) + print(f"\n[*] Installation recorded (ID: {install_id})") + print(f" To rollback: cortex rollback {install_id}") + + return 0 + else: + # Record failed installation + if install_id: + error_msg = result.error_message or "Installation failed" + history.update_installation( + install_id, + InstallationStatus.FAILED, + error_msg + ) + + if result.failed_step is not None: + self._print_error(f"Installation failed at step {result.failed_step + 1}") + else: + self._print_error("Installation failed") + if result.error_message: + print(f" Error: {result.error_message}", file=sys.stderr) + if install_id: + print(f"\nšŸ“ Installation recorded (ID: {install_id})") + print(f" View details: cortex history show {install_id}") + return 1 + else: + print("\nTo execute these commands, run with --execute flag") + print(f"Example: cortex install --template {template_name} --execute") + + return 0 + + except ValueError as e: + if install_id: + history.update_installation(install_id, InstallationStatus.FAILED, str(e)) + self._print_error(str(e)) + return 1 + except Exception as e: + if install_id: + history.update_installation(install_id, InstallationStatus.FAILED, str(e)) + self._print_error(f"Unexpected error: {str(e)}") + return 1 + + def template_list(self): + """List all available templates.""" + try: + template_manager = TemplateManager() + templates = template_manager.list_templates() + + if not templates: + print("No templates found.") + return 0 + + print("\nAvailable Templates:") + print("=" * 80) + print(f"{'Name':<20} {'Version':<12} {'Type':<12} {'Description':<35}") + print("=" * 80) + + for name, info in sorted(templates.items()): + desc = info['description'][:33] + "..." if len(info['description']) > 35 else info['description'] + print(f"{name:<20} {info['version']:<12} {info['type']:<12} {desc:<35}") + + print(f"\nTotal: {len(templates)} templates") + return 0 + except Exception as e: + self._print_error(f"Failed to list templates: {str(e)}") + return 1 + + def template_create(self, name: str, interactive: bool = True): + """Create a new template interactively.""" + try: + print(f"\n[*] Creating template: {name}") + + if interactive: + description = input("Description: ").strip() + if not description: + self._print_error("Description is required") + return 1 + + version = input("Version (default: 1.0.0): ").strip() or "1.0.0" + author = input("Author (optional): ").strip() or None + + print("\nEnter packages (one per line, empty line to finish):") + packages = [] + while True: + pkg = input(" Package: ").strip() + if not pkg: + break + packages.append(pkg) + + # Create template + from cortex.templates import Template, HardwareRequirements + template = Template( + name=name, + description=description, + version=version, + author=author, + packages=packages + ) + + # Ask about hardware requirements + print("\nHardware Requirements (optional):") + min_ram = input(" Minimum RAM (MB, optional): ").strip() + min_cores = input(" Minimum CPU cores (optional): ").strip() + min_storage = input(" Minimum storage (MB, optional): ").strip() + + if min_ram or min_cores or min_storage: + hw_req = HardwareRequirements( + min_ram_mb=int(min_ram) if min_ram else None, + min_cores=int(min_cores) if min_cores else None, + min_storage_mb=int(min_storage) if min_storage else None + ) + template.hardware_requirements = hw_req + + # Save template + template_manager = TemplateManager() + template_path = template_manager.save_template(template, name) + + self._print_success(f"Template '{name}' created successfully!") + print(f" Saved to: {template_path}") + return 0 + else: + self._print_error("Non-interactive template creation not yet supported") + return 1 + + except Exception as e: + self._print_error(f"Failed to create template: {str(e)}") + return 1 + + def template_import(self, file_path: str, name: Optional[str] = None): + """Import a template from a file.""" + try: + template_manager = TemplateManager() + template = template_manager.import_template(file_path, name) + + # Save to user templates + save_name = name or template.name + template_path = template_manager.save_template(template, save_name) + + self._print_success(f"Template '{save_name}' imported successfully!") + print(f" Saved to: {template_path}") + return 0 + except Exception as e: + self._print_error(f"Failed to import template: {str(e)}") + return 1 + + def template_export(self, name: str, file_path: str, format: str = "yaml"): + """Export a template to a file.""" + try: + template_manager = TemplateManager() + template_format = TemplateFormat.YAML if format.lower() == "yaml" else TemplateFormat.JSON + export_path = template_manager.export_template(name, file_path, template_format) + + self._print_success(f"Template '{name}' exported successfully!") + print(f" Saved to: {export_path}") + return 0 + except Exception as e: + self._print_error(f"Failed to export template: {str(e)}") + return 1 def main(): @@ -274,6 +580,11 @@ def main(): cortex install docker --execute cortex install "python 3.11 with pip" cortex install nginx --dry-run + cortex install --template lamp --execute + cortex template list + cortex template create my-stack + cortex template import template.yaml + cortex template export lamp my-template.yaml cortex history cortex history show cortex rollback @@ -287,8 +598,9 @@ def main(): subparsers = parser.add_subparsers(dest='command', help='Available commands') # Install command - install_parser = subparsers.add_parser('install', help='Install software using natural language') - install_parser.add_argument('software', type=str, help='Software to install (natural language)') + install_parser = subparsers.add_parser('install', help='Install software using natural language or template') + install_parser.add_argument('software', type=str, nargs='?', help='Software to install (natural language)') + install_parser.add_argument('--template', type=str, help='Install from template (e.g., lamp, mean, mern)') install_parser.add_argument('--execute', action='store_true', help='Execute the generated commands') install_parser.add_argument('--dry-run', action='store_true', help='Show commands without executing') @@ -304,6 +616,28 @@ def main(): rollback_parser.add_argument('id', help='Installation ID to rollback') rollback_parser.add_argument('--dry-run', action='store_true', help='Show rollback actions without executing') + # Template command + template_parser = subparsers.add_parser('template', help='Manage installation templates') + template_subparsers = template_parser.add_subparsers(dest='template_action', help='Template actions') + + # Template list + template_list_parser = template_subparsers.add_parser('list', help='List all available templates') + + # Template create + template_create_parser = template_subparsers.add_parser('create', help='Create a new template') + template_create_parser.add_argument('name', type=str, help='Template name') + + # Template import + template_import_parser = template_subparsers.add_parser('import', help='Import a template from file') + template_import_parser.add_argument('file_path', type=str, help='Path to template file') + template_import_parser.add_argument('--name', type=str, help='Optional new name for the template') + + # Template export + template_export_parser = template_subparsers.add_parser('export', help='Export a template to file') + template_export_parser.add_argument('name', type=str, help='Template name to export') + template_export_parser.add_argument('file_path', type=str, help='Destination file path') + template_export_parser.add_argument('--format', choices=['yaml', 'json'], default='yaml', help='Export format') + args = parser.parse_args() if not args.command: @@ -314,11 +648,29 @@ def main(): try: if args.command == 'install': - return cli.install(args.software, execute=args.execute, dry_run=args.dry_run) + if args.template: + return cli.install("", execute=args.execute, dry_run=args.dry_run, template=args.template) + elif args.software: + return cli.install(args.software, execute=args.execute, dry_run=args.dry_run) + else: + install_parser.print_help() + return 1 elif args.command == 'history': return cli.history(limit=args.limit, status=args.status, show_id=args.show_id) elif args.command == 'rollback': return cli.rollback(args.id, dry_run=args.dry_run) + elif args.command == 'template': + if args.template_action == 'list': + return cli.template_list() + elif args.template_action == 'create': + return cli.template_create(args.name) + elif args.template_action == 'import': + return cli.template_import(args.file_path, args.name) + elif args.template_action == 'export': + return cli.template_export(args.name, args.file_path, args.format) + else: + template_parser.print_help() + return 1 else: parser.print_help() return 1 diff --git a/cortex/templates.py b/cortex/templates.py new file mode 100644 index 00000000..f4dd7f64 --- /dev/null +++ b/cortex/templates.py @@ -0,0 +1,518 @@ +#!/usr/bin/env python3 +""" +Template System for Cortex Linux Installation Templates + +Supports pre-built templates for common development stacks (LAMP, MEAN, MERN, etc.) +and custom template creation, validation, and hardware-aware selection. +""" + +import json +import yaml +import os +import sys +from pathlib import Path +from typing import Dict, List, Optional, Any, Set, Tuple +from dataclasses import dataclass, field +from enum import Enum + +# Add parent directory to path for imports +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from src.hwprofiler import HardwareProfiler +from cortex.packages import PackageManager, PackageManagerType + + +class TemplateFormat(Enum): + """Supported template formats.""" + YAML = "yaml" + JSON = "json" + + +@dataclass +class HardwareRequirements: + """Hardware requirements for a template.""" + min_ram_mb: Optional[int] = None + min_cores: Optional[int] = None + min_storage_mb: Optional[int] = None + requires_gpu: bool = False + gpu_vendor: Optional[str] = None # "NVIDIA", "AMD", "Intel" + requires_cuda: bool = False + min_cuda_version: Optional[str] = None + + +@dataclass +class InstallationStep: + """A single installation step in a template.""" + command: str + description: str + rollback: Optional[str] = None + verify: Optional[str] = None + requires_root: bool = True + + +@dataclass +class Template: + """Represents an installation template.""" + name: str + description: str + version: str + author: Optional[str] = None + packages: List[str] = field(default_factory=list) + steps: List[InstallationStep] = field(default_factory=list) + hardware_requirements: Optional[HardwareRequirements] = None + post_install: List[str] = field(default_factory=list) + verification_commands: List[str] = field(default_factory=list) + metadata: Dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> Dict[str, Any]: + """Convert template to dictionary.""" + result = { + "name": self.name, + "description": self.description, + "version": self.version, + "packages": self.packages, + "post_install": self.post_install, + "verification_commands": self.verification_commands, + "metadata": self.metadata + } + + if self.author: + result["author"] = self.author + + if self.steps: + result["steps"] = [ + { + "command": step.command, + "description": step.description, + "rollback": step.rollback, + "verify": step.verify, + "requires_root": step.requires_root + } + for step in self.steps + ] + + if self.hardware_requirements: + hw = self.hardware_requirements + result["hardware_requirements"] = { + "min_ram_mb": hw.min_ram_mb, + "min_cores": hw.min_cores, + "min_storage_mb": hw.min_storage_mb, + "requires_gpu": hw.requires_gpu, + "gpu_vendor": hw.gpu_vendor, + "requires_cuda": hw.requires_cuda, + "min_cuda_version": hw.min_cuda_version + } + + return result + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> "Template": + """Create template from dictionary.""" + # Parse hardware requirements + hw_req = None + if "hardware_requirements" in data: + hw_data = data["hardware_requirements"] + hw_req = HardwareRequirements( + min_ram_mb=hw_data.get("min_ram_mb"), + min_cores=hw_data.get("min_cores"), + min_storage_mb=hw_data.get("min_storage_mb"), + requires_gpu=hw_data.get("requires_gpu", False), + gpu_vendor=hw_data.get("gpu_vendor"), + requires_cuda=hw_data.get("requires_cuda", False), + min_cuda_version=hw_data.get("min_cuda_version") + ) + + # Parse installation steps + steps = [] + if "steps" in data: + for step_data in data["steps"]: + steps.append(InstallationStep( + command=step_data["command"], + description=step_data.get("description", ""), + rollback=step_data.get("rollback"), + verify=step_data.get("verify"), + requires_root=step_data.get("requires_root", True) + )) + + return cls( + name=data["name"], + description=data["description"], + version=data.get("version", "1.0.0"), + author=data.get("author"), + packages=data.get("packages", []), + steps=steps, + hardware_requirements=hw_req, + post_install=data.get("post_install", []), + verification_commands=data.get("verification_commands", []), + metadata=data.get("metadata", {}) + ) + + +class TemplateValidator: + """Validates template structure and content.""" + + REQUIRED_FIELDS = ["name", "description", "version"] + REQUIRED_STEP_FIELDS = ["command", "description"] + + @staticmethod + def validate(template: Template) -> Tuple[bool, List[str]]: + """ + Validate a template. + + Returns: + Tuple of (is_valid, list_of_errors) + """ + errors = [] + + # Check required fields + if not template.name: + errors.append("Template name is required") + if not template.description: + errors.append("Template description is required") + if not template.version: + errors.append("Template version is required") + + # Validate steps + for i, step in enumerate(template.steps): + if not step.command: + errors.append(f"Step {i+1}: command is required") + if not step.description: + errors.append(f"Step {i+1}: description is required") + + # Validate packages list + if not template.packages and not template.steps: + errors.append("Template must have either packages or steps defined") + + # Validate hardware requirements + if template.hardware_requirements: + hw = template.hardware_requirements + if hw.min_ram_mb is not None and hw.min_ram_mb < 0: + errors.append("min_ram_mb must be non-negative") + if hw.min_cores is not None and hw.min_cores < 0: + errors.append("min_cores must be non-negative") + if hw.min_storage_mb is not None and hw.min_storage_mb < 0: + errors.append("min_storage_mb must be non-negative") + if hw.requires_cuda and not hw.requires_gpu: + errors.append("requires_cuda is true but requires_gpu is false") + + return len(errors) == 0, errors + + +class TemplateManager: + """Manages installation templates.""" + + def __init__(self, templates_dir: Optional[str] = None): + """ + Initialize template manager. + + Args: + templates_dir: Directory containing templates (defaults to built-in templates) + """ + if templates_dir: + self.templates_dir = Path(templates_dir) + else: + # Default to built-in templates directory + base_dir = Path(__file__).parent + self.templates_dir = base_dir / "templates" + + self.user_templates_dir = Path.home() / ".cortex" / "templates" + self.user_templates_dir.mkdir(parents=True, exist_ok=True) + + self._templates_cache: Dict[str, Template] = {} + self._hardware_profiler = HardwareProfiler() + self._package_manager = PackageManager() + + def _get_template_path(self, name: str) -> Optional[Path]: + """Find template file by name.""" + # Check user templates first + for ext in [".yaml", ".yml", ".json"]: + user_path = self.user_templates_dir / f"{name}{ext}" + if user_path.exists(): + return user_path + + # Check built-in templates + for ext in [".yaml", ".yml", ".json"]: + builtin_path = self.templates_dir / f"{name}{ext}" + if builtin_path.exists(): + return builtin_path + + return None + + def load_template(self, name: str) -> Optional[Template]: + """Load a template by name.""" + if name in self._templates_cache: + return self._templates_cache[name] + + template_path = self._get_template_path(name) + if not template_path: + return None + + try: + with open(template_path, 'r', encoding='utf-8') as f: + if template_path.suffix in ['.yaml', '.yml']: + data = yaml.safe_load(f) + else: + data = json.load(f) + + template = Template.from_dict(data) + self._templates_cache[name] = template + return template + except Exception as e: + raise ValueError(f"Failed to load template {name}: {str(e)}") + + def save_template(self, template: Template, name: Optional[str] = None, + format: TemplateFormat = TemplateFormat.YAML) -> Path: + """ + Save a template to user templates directory. + + Args: + template: Template to save + name: Template name (defaults to template.name) + format: File format (YAML or JSON) + + Returns: + Path to saved template file + """ + # Validate template + is_valid, errors = TemplateValidator.validate(template) + if not is_valid: + raise ValueError(f"Template validation failed: {', '.join(errors)}") + + template_name = name or template.name + ext = ".yaml" if format == TemplateFormat.YAML else ".json" + template_path = self.user_templates_dir / f"{template_name}{ext}" + + data = template.to_dict() + + with open(template_path, 'w', encoding='utf-8') as f: + if format == TemplateFormat.YAML: + yaml.dump(data, f, default_flow_style=False, sort_keys=False) + else: + json.dump(data, f, indent=2) + + return template_path + + def list_templates(self) -> Dict[str, Dict[str, Any]]: + """List all available templates.""" + templates = {} + + # List built-in templates + if self.templates_dir.exists(): + for ext in [".yaml", ".yml", ".json"]: + for template_file in self.templates_dir.glob(f"*{ext}"): + name = template_file.stem + try: + template = self.load_template(name) + if template: + templates[name] = { + "name": template.name, + "description": template.description, + "version": template.version, + "author": template.author, + "type": "built-in", + "path": str(template_file) + } + except Exception: + pass + + # List user templates + if self.user_templates_dir.exists(): + for ext in [".yaml", ".yml", ".json"]: + for template_file in self.user_templates_dir.glob(f"*{ext}"): + name = template_file.stem + if name not in templates: # Don't override built-in + try: + template = self.load_template(name) + if template: + templates[name] = { + "name": template.name, + "description": template.description, + "version": template.version, + "author": template.author, + "type": "user", + "path": str(template_file) + } + except Exception: + pass + + return templates + + def check_hardware_compatibility(self, template: Template) -> Tuple[bool, List[str]]: + """ + Check if current hardware meets template requirements. + + Returns: + Tuple of (is_compatible, list_of_warnings) + """ + if not template.hardware_requirements: + return True, [] + + hw_profile = self._hardware_profiler.profile() + hw_req = template.hardware_requirements + warnings = [] + + # Check RAM + if hw_req.min_ram_mb: + available_ram = hw_profile.get("ram", 0) + if available_ram < hw_req.min_ram_mb: + warnings.append( + f"Insufficient RAM: {available_ram}MB available, " + f"{hw_req.min_ram_mb}MB required" + ) + + # Check CPU cores + if hw_req.min_cores: + available_cores = hw_profile.get("cpu", {}).get("cores", 0) + if available_cores < hw_req.min_cores: + warnings.append( + f"Insufficient CPU cores: {available_cores} available, " + f"{hw_req.min_cores} required" + ) + + # Check storage + if hw_req.min_storage_mb: + total_storage = sum( + s.get("size", 0) for s in hw_profile.get("storage", []) + ) + if total_storage < hw_req.min_storage_mb: + warnings.append( + f"Insufficient storage: {total_storage}MB available, " + f"{hw_req.min_storage_mb}MB required" + ) + + # Check GPU requirements + if hw_req.requires_gpu: + gpus = hw_profile.get("gpu", []) + if not gpus: + warnings.append("GPU required but not detected") + elif hw_req.gpu_vendor: + vendor_match = any( + g.get("vendor") == hw_req.gpu_vendor for g in gpus + ) + if not vendor_match: + warnings.append( + f"{hw_req.gpu_vendor} GPU required but not found" + ) + + # Check CUDA requirements + if hw_req.requires_cuda: + gpus = hw_profile.get("gpu", []) + cuda_found = False + for gpu in gpus: + if gpu.get("vendor") == "NVIDIA" and gpu.get("cuda"): + cuda_version = gpu.get("cuda", "") + if hw_req.min_cuda_version: + # Simple version comparison + try: + gpu_cuda = tuple(map(int, cuda_version.split('.'))) + req_cuda = tuple(map(int, hw_req.min_cuda_version.split('.'))) + if gpu_cuda >= req_cuda: + cuda_found = True + break + except ValueError: + # If version parsing fails, just check if CUDA exists + cuda_found = True + break + else: + cuda_found = True + break + + if not cuda_found: + warnings.append( + f"CUDA {hw_req.min_cuda_version or ''} required but not found" + ) + + is_compatible = len(warnings) == 0 + return is_compatible, warnings + + def generate_commands(self, template: Template) -> List[str]: + """ + Generate installation commands from template. + + Returns: + List of installation commands + """ + commands = [] + + # If template has explicit steps, use those + if template.steps: + commands = [step.command for step in template.steps] + # Otherwise, generate from packages + elif template.packages: + # Use package manager to generate commands + pm = PackageManager() + package_list = " ".join(template.packages) + try: + commands = pm.parse(f"install {package_list}") + except ValueError: + # Fallback: direct apt/yum install + pm_type = pm.pm_type.value + commands = [f"{pm_type} install -y {' '.join(template.packages)}"] + + return commands + + def import_template(self, file_path: str, name: Optional[str] = None) -> Template: + """ + Import a template from a file. + + Args: + file_path: Path to template file + name: Optional new name for the template + + Returns: + Loaded template + """ + template_path = Path(file_path) + if not template_path.exists(): + raise FileNotFoundError(f"Template file not found: {file_path}") + + try: + with open(template_path, 'r', encoding='utf-8') as f: + if template_path.suffix in ['.yaml', '.yml']: + data = yaml.safe_load(f) + else: + data = json.load(f) + + template = Template.from_dict(data) + + # Override name if provided + if name: + template.name = name + + # Validate + is_valid, errors = TemplateValidator.validate(template) + if not is_valid: + raise ValueError(f"Template validation failed: {', '.join(errors)}") + + return template + except Exception as e: + raise ValueError(f"Failed to import template: {str(e)}") + + def export_template(self, name: str, file_path: str, + format: TemplateFormat = TemplateFormat.YAML) -> Path: + """ + Export a template to a file. + + Args: + name: Template name + file_path: Destination file path + format: File format + + Returns: + Path to exported file + """ + template = self.load_template(name) + if not template: + raise ValueError(f"Template not found: {name}") + + export_path = Path(file_path) + data = template.to_dict() + + with open(export_path, 'w', encoding='utf-8') as f: + if format == TemplateFormat.YAML: + yaml.dump(data, f, default_flow_style=False, sort_keys=False) + else: + json.dump(data, f, indent=2) + + return export_path + diff --git a/cortex/templates/README.md b/cortex/templates/README.md new file mode 100644 index 00000000..44dd788b --- /dev/null +++ b/cortex/templates/README.md @@ -0,0 +1,65 @@ +# Installation Templates + +This directory contains built-in installation templates for common development stacks. + +## Available Templates + +- **lamp.yaml** - LAMP Stack (Linux, Apache, MySQL, PHP) +- **mean.yaml** - MEAN Stack (MongoDB, Express.js, Angular, Node.js) +- **mern.yaml** - MERN Stack (MongoDB, Express.js, React, Node.js) +- **ml-ai.yaml** - Machine Learning / AI Stack +- **devops.yaml** - DevOps Stack (Docker, Kubernetes, Terraform, etc.) + +## Usage + +```bash +# List all templates +cortex template list + +# Install from template +cortex install --template lamp --execute + +# Create custom template +cortex template create my-stack + +# Import template +cortex template import my-template.yaml + +# Export template +cortex template export lamp my-lamp.yaml +``` + +## Template Format + +Templates are defined in YAML format with the following structure: + +```yaml +name: Template Name +description: Template description +version: 1.0.0 +author: Author Name (optional) + +packages: + - package1 + - package2 + +steps: + - command: apt update + description: Update packages + requires_root: true + rollback: (optional) + +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 + min_storage_mb: 10240 + +post_install: + - echo "Installation complete" + +verification_commands: + - package --version +``` + +See [TEMPLATES.md](../../docs/TEMPLATES.md) for complete documentation. + diff --git a/cortex/templates/devops.yaml b/cortex/templates/devops.yaml new file mode 100644 index 00000000..f9c87515 --- /dev/null +++ b/cortex/templates/devops.yaml @@ -0,0 +1,93 @@ +name: DevOps Stack +description: Complete DevOps toolchain with Docker, Kubernetes, Terraform, Ansible, and CI/CD tools +version: 1.0.0 +author: Cortex Linux +packages: + - docker.io + - docker-compose + - kubectl + - git + - curl + - wget + - ansible + - terraform + - jenkins + - gitlab-runner + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y apt-transport-https ca-certificates curl gnupg lsb-release + description: Install prerequisites for Docker + requires_root: true + - command: install -m 0755 -d /etc/apt/keyrings + description: Create keyrings directory + requires_root: true + - command: curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + description: Add Docker GPG key + requires_root: true + - command: chmod a+r /etc/apt/keyrings/docker.gpg + description: Set key permissions + requires_root: true + - command: echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + description: Add Docker repository + requires_root: true + - command: apt update + description: Update package lists with Docker repo + requires_root: true + - command: apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + description: Install Docker + requires_root: true + - command: systemctl start docker + description: Start Docker service + requires_root: true + rollback: systemctl stop docker + - command: systemctl enable docker + description: Enable Docker on boot + requires_root: true + rollback: systemctl disable docker + - command: curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" + description: Download kubectl + requires_root: false + - command: install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl + description: Install kubectl + requires_root: true + - command: rm kubectl + description: Clean up kubectl download + requires_root: false + - command: apt install -y ansible terraform git + description: Install Ansible, Terraform, and Git + requires_root: true + +hardware_requirements: + min_ram_mb: 4096 + min_cores: 4 + min_storage_mb: 20480 + +post_install: + - echo "DevOps stack installed successfully" + - echo "Docker version: $(docker --version)" + - echo "Kubernetes: $(kubectl version --client --short 2>/dev/null || echo 'installed')" + - echo "Terraform: $(terraform --version | head -n1)" + - echo "Ansible: $(ansible --version | head -n1)" + +verification_commands: + - docker --version + - docker ps + - kubectl version --client + - terraform --version + - ansible --version + - git --version + - systemctl is-active docker + +metadata: + category: devops + tags: + - docker + - kubernetes + - terraform + - ansible + - ci-cd + - infrastructure + diff --git a/cortex/templates/lamp.yaml b/cortex/templates/lamp.yaml new file mode 100644 index 00000000..5421437f --- /dev/null +++ b/cortex/templates/lamp.yaml @@ -0,0 +1,68 @@ +name: LAMP Stack +description: Linux, Apache, MySQL, PHP stack for web development +version: 1.0.0 +author: Cortex Linux +packages: + - apache2 + - mysql-server + - mysql-client + - php + - php-mysql + - php-mbstring + - php-xml + - php-curl + - phpmyadmin + - libapache2-mod-php + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y apache2 mysql-server mysql-client php php-mysql php-mbstring php-xml php-curl libapache2-mod-php + description: Install LAMP stack packages + requires_root: true + - command: apt install -y phpmyadmin + description: Install phpMyAdmin + requires_root: true + - command: systemctl start apache2 + description: Start Apache web server + requires_root: true + rollback: systemctl stop apache2 + - command: systemctl enable apache2 + description: Enable Apache on boot + requires_root: true + rollback: systemctl disable apache2 + - command: systemctl start mysql + description: Start MySQL service + requires_root: true + rollback: systemctl stop mysql + - command: systemctl enable mysql + description: Enable MySQL on boot + requires_root: true + rollback: systemctl disable mysql + +hardware_requirements: + min_ram_mb: 1024 + min_cores: 2 + min_storage_mb: 5120 + +post_install: + - echo "LAMP stack installed successfully" + - echo "Apache: http://localhost" + - echo "phpMyAdmin: http://localhost/phpmyadmin" + +verification_commands: + - apache2 -v + - mysql --version + - php -v + - systemctl is-active apache2 + - systemctl is-active mysql + +metadata: + category: web-development + tags: + - web + - server + - database + - php + diff --git a/cortex/templates/mean.yaml b/cortex/templates/mean.yaml new file mode 100644 index 00000000..21bb9c68 --- /dev/null +++ b/cortex/templates/mean.yaml @@ -0,0 +1,64 @@ +name: MEAN Stack +description: MongoDB, Express.js, Angular, Node.js stack for modern web applications +version: 1.0.0 +author: Cortex Linux +packages: + - nodejs + - npm + - mongodb + - git + - curl + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + description: Add Node.js repository + requires_root: true + - command: apt install -y nodejs + description: Install Node.js + requires_root: true + - command: npm install -g @angular/cli express-generator + description: Install Angular CLI and Express generator globally + requires_root: false + - command: apt install -y mongodb + description: Install MongoDB + requires_root: true + - command: systemctl start mongodb + description: Start MongoDB service + requires_root: true + rollback: systemctl stop mongodb + - command: systemctl enable mongodb + description: Enable MongoDB on boot + requires_root: true + rollback: systemctl disable mongodb + +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 + min_storage_mb: 10240 + +post_install: + - echo "MEAN stack installed successfully" + - echo "Node.js version: $(node --version)" + - echo "npm version: $(npm --version)" + - echo "Angular CLI: $(ng version 2>/dev/null || echo 'installed')" + - echo "MongoDB: mongodb://localhost:27017" + +verification_commands: + - node --version + - npm --version + - mongod --version + - systemctl is-active mongodb + - ng version + +metadata: + category: web-development + tags: + - javascript + - nodejs + - mongodb + - angular + - express + diff --git a/cortex/templates/mern.yaml b/cortex/templates/mern.yaml new file mode 100644 index 00000000..953606ac --- /dev/null +++ b/cortex/templates/mern.yaml @@ -0,0 +1,64 @@ +name: MERN Stack +description: MongoDB, Express.js, React, Node.js stack for full-stack JavaScript development +version: 1.0.0 +author: Cortex Linux +packages: + - nodejs + - npm + - mongodb + - git + - curl + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + description: Add Node.js repository + requires_root: true + - command: apt install -y nodejs + description: Install Node.js + requires_root: true + - command: npm install -g create-react-app express-generator + description: Install React and Express generators globally + requires_root: false + - command: apt install -y mongodb + description: Install MongoDB + requires_root: true + - command: systemctl start mongodb + description: Start MongoDB service + requires_root: true + rollback: systemctl stop mongodb + - command: systemctl enable mongodb + description: Enable MongoDB on boot + requires_root: true + rollback: systemctl disable mongodb + +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 + min_storage_mb: 10240 + +post_install: + - echo "MERN stack installed successfully" + - echo "Node.js version: $(node --version)" + - echo "npm version: $(npm --version)" + - echo "React: Create apps with 'npx create-react-app'" + - echo "MongoDB: mongodb://localhost:27017" + +verification_commands: + - node --version + - npm --version + - mongod --version + - systemctl is-active mongodb + - npx create-react-app --version + +metadata: + category: web-development + tags: + - javascript + - nodejs + - mongodb + - react + - express + diff --git a/cortex/templates/ml-ai.yaml b/cortex/templates/ml-ai.yaml new file mode 100644 index 00000000..58809611 --- /dev/null +++ b/cortex/templates/ml-ai.yaml @@ -0,0 +1,66 @@ +name: ML/AI Stack +description: Machine Learning and Artificial Intelligence development stack with Python, TensorFlow, PyTorch, and Jupyter +version: 1.0.0 +author: Cortex Linux +packages: + - python3 + - python3-pip + - python3-venv + - python3-dev + - build-essential + - git + - curl + - wget + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y python3 python3-pip python3-venv python3-dev build-essential git curl wget + description: Install Python and build tools + requires_root: true + - command: pip3 install --upgrade pip + description: Upgrade pip to latest version + requires_root: false + - command: pip3 install numpy pandas scipy matplotlib scikit-learn jupyter notebook + description: Install core ML libraries + requires_root: false + - command: pip3 install tensorflow torch torchvision torchaudio + description: Install deep learning frameworks + requires_root: false + - command: pip3 install seaborn plotly opencv-python-headless + description: Install additional ML/visualization libraries + requires_root: false + +hardware_requirements: + min_ram_mb: 4096 + min_cores: 4 + min_storage_mb: 20480 + requires_gpu: false + requires_cuda: false + +post_install: + - echo "ML/AI stack installed successfully" + - echo "Python version: $(python3 --version)" + - echo "Start Jupyter: jupyter notebook" + - echo "TensorFlow: $(python3 -c 'import tensorflow as tf; print(tf.__version__)' 2>/dev/null || echo 'installed')" + - echo "PyTorch: $(python3 -c 'import torch; print(torch.__version__)' 2>/dev/null || echo 'installed')" + +verification_commands: + - python3 --version + - pip3 --version + - python3 -c "import numpy; print('NumPy:', numpy.__version__)" + - python3 -c "import pandas; print('Pandas:', pandas.__version__)" + - python3 -c "import sklearn; print('Scikit-learn:', sklearn.__version__)" + - jupyter --version + +metadata: + category: machine-learning + tags: + - python + - machine-learning + - ai + - tensorflow + - pytorch + - jupyter + diff --git a/docs/TEMPLATES.md b/docs/TEMPLATES.md new file mode 100644 index 00000000..4fc12347 --- /dev/null +++ b/docs/TEMPLATES.md @@ -0,0 +1,570 @@ +# Installation Templates Guide + +Cortex Linux provides a powerful template system for installing common development stacks and software bundles. Templates are pre-configured installation definitions that can be shared, customized, and reused. + +## Overview + +Templates allow you to: +- Install complete development stacks with a single command +- Share installation configurations with your team +- Create custom templates for your specific needs +- Validate hardware compatibility before installation +- Export and import templates for easy sharing + +## Quick Start + +### Installing from a Template + +```bash +# List available templates +cortex template list + +# Install LAMP stack +cortex install --template lamp --execute + +# Install MEAN stack (dry run first) +cortex install --template mean --dry-run +cortex install --template mean --execute +``` + +### Creating a Custom Template + +```bash +# Create a new template interactively +cortex template create my-stack + +# Import a template from file +cortex template import my-template.yaml + +# Export a template +cortex template export lamp my-lamp-template.yaml +``` + +## Built-in Templates + +Cortex Linux comes with 5+ pre-built templates: + +### 1. LAMP Stack + +Linux, Apache, MySQL, PHP stack for traditional web development. + +```bash +cortex install --template lamp --execute +``` + +**Packages:** +- Apache 2.4 +- MySQL 8.0 +- PHP 8.2 +- phpMyAdmin + +**Hardware Requirements:** +- Minimum RAM: 1GB +- Minimum CPU cores: 2 +- Minimum storage: 5GB + +**Access:** +- Apache: http://localhost +- phpMyAdmin: http://localhost/phpmyadmin + +### 2. MEAN Stack + +MongoDB, Express.js, Angular, Node.js stack for modern web applications. + +```bash +cortex install --template mean --execute +``` + +**Packages:** +- Node.js 20.x +- MongoDB +- Angular CLI +- Express generator + +**Hardware Requirements:** +- Minimum RAM: 2GB +- Minimum CPU cores: 2 +- Minimum storage: 10GB + +### 3. MERN Stack + +MongoDB, Express.js, React, Node.js stack for full-stack JavaScript development. + +```bash +cortex install --template mern --execute +``` + +**Packages:** +- Node.js 20.x +- MongoDB +- React (via create-react-app) +- Express generator + +**Hardware Requirements:** +- Minimum RAM: 2GB +- Minimum CPU cores: 2 +- Minimum storage: 10GB + +### 4. ML/AI Stack + +Machine Learning and Artificial Intelligence development stack. + +```bash +cortex install --template ml-ai --execute +``` + +**Packages:** +- Python 3.x +- NumPy, Pandas, SciPy +- TensorFlow +- PyTorch +- Jupyter Notebook +- Scikit-learn +- Matplotlib, Seaborn + +**Hardware Requirements:** +- Minimum RAM: 4GB +- Minimum CPU cores: 4 +- Minimum storage: 20GB + +### 5. DevOps Stack + +Complete DevOps toolchain with containerization and infrastructure tools. + +```bash +cortex install --template devops --execute +``` + +**Packages:** +- Docker & Docker Compose +- Kubernetes (kubectl) +- Terraform +- Ansible +- Git +- Jenkins (optional) + +**Hardware Requirements:** +- Minimum RAM: 4GB +- Minimum CPU cores: 4 +- Minimum storage: 20GB + +## Template Format + +Templates are defined in YAML or JSON format. Here's the structure: + +### YAML Format + +```yaml +name: My Custom Stack +description: A custom development stack +version: 1.0.0 +author: Your Name + +packages: + - package1 + - package2 + - package3 + +steps: + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y package1 package2 + description: Install packages + requires_root: true + rollback: apt remove -y package1 package2 + +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 + min_storage_mb: 10240 + requires_gpu: false + requires_cuda: false + +post_install: + - echo "Stack installed successfully" + - echo "Access at: http://localhost" + +verification_commands: + - package1 --version + - systemctl is-active service1 + +metadata: + category: web-development + tags: + - web + - server +``` + +### JSON Format + +```json +{ + "name": "My Custom Stack", + "description": "A custom development stack", + "version": "1.0.0", + "author": "Your Name", + "packages": [ + "package1", + "package2" + ], + "steps": [ + { + "command": "apt update", + "description": "Update package lists", + "requires_root": true + } + ], + "hardware_requirements": { + "min_ram_mb": 2048, + "min_cores": 2, + "min_storage_mb": 10240 + }, + "post_install": [ + "echo 'Stack installed successfully'" + ], + "verification_commands": [ + "package1 --version" + ] +} +``` + +## Template Fields + +### Required Fields + +- **name**: Template name (string) +- **description**: Template description (string) +- **version**: Template version (string, e.g., "1.0.0") + +### Optional Fields + +- **author**: Template author (string) +- **packages**: List of package names (array of strings) +- **steps**: Installation steps (array of step objects) +- **hardware_requirements**: Hardware requirements (object) +- **post_install**: Post-installation commands (array of strings) +- **verification_commands**: Commands to verify installation (array of strings) +- **metadata**: Additional metadata (object) + +### Installation Steps + +Each step can have: +- **command**: Command to execute (required) +- **description**: Step description (required) +- **rollback**: Rollback command (optional) +- **verify**: Verification command (optional) +- **requires_root**: Whether root is required (boolean, default: true) + +### Hardware Requirements + +- **min_ram_mb**: Minimum RAM in megabytes (integer) +- **min_cores**: Minimum CPU cores (integer) +- **min_storage_mb**: Minimum storage in megabytes (integer) +- **requires_gpu**: Whether GPU is required (boolean) +- **gpu_vendor**: Required GPU vendor ("NVIDIA", "AMD", "Intel") +- **requires_cuda**: Whether CUDA is required (boolean) +- **min_cuda_version**: Minimum CUDA version (string, e.g., "11.0") + +## Creating Custom Templates + +### Interactive Creation + +```bash +cortex template create my-stack +``` + +This will prompt you for: +- Description +- Version +- Author (optional) +- Packages (one per line) +- Hardware requirements (optional) + +### Manual Creation + +1. Create a YAML or JSON file: + +```yaml +name: my-custom-stack +description: My custom development stack +version: 1.0.0 +packages: + - python3 + - nodejs + - docker +``` + +2. Import the template: + +```bash +cortex template import my-template.yaml +``` + +3. Use the template: + +```bash +cortex install --template my-custom-stack --execute +``` + +## Template Management + +### Listing Templates + +```bash +cortex template list +``` + +Output: +``` +šŸ“‹ Available Templates: +================================================================================ +Name Version Type Description +================================================================================ +devops 1.0.0 built-in Complete DevOps toolchain... +lamp 1.0.0 built-in Linux, Apache, MySQL, PHP... +mean 1.0.0 built-in MongoDB, Express.js, Angular... +mern 1.0.0 built-in MongoDB, Express.js, React... +ml-ai 1.0.0 built-in Machine Learning and AI... + +Total: 5 templates +``` + +### Exporting Templates + +```bash +# Export to YAML (default) +cortex template export lamp my-lamp-template.yaml + +# Export to JSON +cortex template export lamp my-lamp-template.json --format json +``` + +### Importing Templates + +```bash +# Import with original name +cortex template import my-template.yaml + +# Import with custom name +cortex template import my-template.yaml --name my-custom-name +``` + +## Hardware Compatibility + +Templates can specify hardware requirements. Cortex will check compatibility before installation: + +```bash +$ cortex install --template ml-ai --execute + +šŸ“‹ ML/AI Stack Template: + Machine Learning and Artificial Intelligence development stack + + Packages: + - python3 + - python3-pip + ... + +āš ļø Hardware Compatibility Warnings: + - Insufficient RAM: 2048MB available, 4096MB required + +āš ļø Hardware requirements not met. Continue anyway? (y/N): +``` + +## Template Validation + +Templates are automatically validated before installation. Validation checks: + +- Required fields are present +- At least packages or steps are defined +- Step commands and descriptions are provided +- Hardware requirements are valid (non-negative values) +- CUDA requirements are consistent with GPU requirements + +## Example Templates + +### Python Data Science Stack + +```yaml +name: Python Data Science +description: Python with data science libraries +version: 1.0.0 +packages: + - python3 + - python3-pip + - python3-venv +steps: + - command: pip3 install numpy pandas scipy matplotlib jupyter scikit-learn + description: Install data science libraries + requires_root: false +hardware_requirements: + min_ram_mb: 2048 + min_cores: 2 +``` + +### Docker Development Stack + +```yaml +name: Docker Development +description: Docker with development tools +version: 1.0.0 +packages: + - docker.io + - docker-compose + - git +steps: + - command: systemctl start docker + description: Start Docker service + requires_root: true + rollback: systemctl stop docker + - command: systemctl enable docker + description: Enable Docker on boot + requires_root: true +verification_commands: + - docker --version + - docker ps +``` + +### Full-Stack Web Development + +```yaml +name: Full-Stack Web +description: Complete web development environment +version: 1.0.0 +packages: + - nodejs + - npm + - python3 + - postgresql + - redis-server + - nginx +steps: + - command: npm install -g yarn typescript + description: Install global Node.js tools + requires_root: false + - command: systemctl start postgresql + description: Start PostgreSQL + requires_root: true + - command: systemctl start redis + description: Start Redis + requires_root: true +hardware_requirements: + min_ram_mb: 4096 + min_cores: 4 +post_install: + - echo "Web development stack ready!" + - echo "PostgreSQL: localhost:5432" + - echo "Redis: localhost:6379" +``` + +## Best Practices + +1. **Always test templates in dry-run mode first:** + ```bash + cortex install --template my-template --dry-run + ``` + +2. **Specify hardware requirements** to help users understand system needs + +3. **Include verification commands** to ensure installation succeeded + +4. **Add rollback commands** for critical steps to enable safe rollback + +5. **Use descriptive step descriptions** for better user experience + +6. **Version your templates** to track changes + +7. **Document post-installation steps** in post_install commands + +## Troubleshooting + +### Template Not Found + +If a template is not found, check: +- Template name is correct (use `cortex template list` to verify) +- Template file exists in `~/.cortex/templates/` or built-in templates directory +- File extension is `.yaml`, `.yml`, or `.json` + +### Validation Errors + +If template validation fails: +- Check all required fields are present +- Ensure at least packages or steps are defined +- Verify hardware requirements are non-negative +- Check step commands and descriptions are provided + +### Hardware Compatibility Warnings + +If hardware compatibility warnings appear: +- Review the warnings carefully +- Consider if the installation will work with your hardware +- Some templates may work with less hardware but with reduced performance +- You can proceed anyway if you understand the risks + +## Template Sharing + +Templates can be shared by: +1. Exporting to a file +2. Sharing the file via version control, email, or file sharing +3. Importing on another system + +Example workflow: +```bash +# On source system +cortex template export my-stack my-stack.yaml + +# Share my-stack.yaml + +# On target system +cortex template import my-stack.yaml +cortex install --template my-stack --execute +``` + +## Advanced Usage + +### Using Steps Instead of Packages + +For more control, use explicit installation steps: + +```yaml +steps: + - command: apt update + description: Update package lists + - command: curl -fsSL https://get.docker.com | sh + description: Install Docker + rollback: apt remove -y docker docker-engine + - command: systemctl start docker + description: Start Docker service + verify: systemctl is-active docker +``` + +### Conditional Installation + +While templates don't support conditional logic directly, you can use shell commands: + +```yaml +steps: + - command: | + if [ ! -f /usr/bin/docker ]; then + apt install -y docker.io + fi + description: Install Docker if not present +``` + +### Post-Installation Configuration + +Use post_install commands for configuration: + +```yaml +post_install: + - echo "Configuring service..." + - systemctl enable myservice + - echo "Service configured. Access at http://localhost:8080" +``` + +## See Also + +- [User Guide](../User-Guide.md) - General Cortex usage +- [Developer Guide](../Developer-Guide.md) - Contributing to Cortex +- [Getting Started](../Getting-Started.md) - Quick start guide + diff --git a/src/requirements.txt b/src/requirements.txt index 65c3c151..4e2d6205 100644 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -4,6 +4,7 @@ # Core Dependencies rich>=13.0.0 # Beautiful terminal progress bars and formatting plyer>=2.0.0 # Desktop notifications (optional but recommended) +pyyaml>=6.0.0 # YAML parsing for template system # Testing Dependencies (dev) pytest>=7.0.0 diff --git a/test/test_templates.py b/test/test_templates.py new file mode 100644 index 00000000..553449a3 --- /dev/null +++ b/test/test_templates.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +""" +Unit tests for Cortex Linux Template System +""" + +import unittest +import tempfile +import shutil +import os +import json +import yaml +from pathlib import Path + +import sys +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +from cortex.templates import ( + Template, + TemplateManager, + TemplateValidator, + TemplateFormat, + HardwareRequirements, + InstallationStep +) + + +class TestTemplate(unittest.TestCase): + """Test Template dataclass.""" + + def test_template_creation(self): + """Test creating a template.""" + template = Template( + name="test-template", + description="Test template", + version="1.0.0", + author="Test Author", + packages=["package1", "package2"] + ) + + self.assertEqual(template.name, "test-template") + self.assertEqual(template.description, "Test template") + self.assertEqual(template.version, "1.0.0") + self.assertEqual(template.author, "Test Author") + self.assertEqual(len(template.packages), 2) + + def test_template_to_dict(self): + """Test converting template to dictionary.""" + template = Template( + name="test", + description="Test", + version="1.0.0", + packages=["pkg1", "pkg2"] + ) + + data = template.to_dict() + + self.assertEqual(data["name"], "test") + self.assertEqual(data["description"], "Test") + self.assertEqual(data["version"], "1.0.0") + self.assertEqual(data["packages"], ["pkg1", "pkg2"]) + + def test_template_from_dict(self): + """Test creating template from dictionary.""" + data = { + "name": "test", + "description": "Test description", + "version": "1.0.0", + "packages": ["pkg1", "pkg2"], + "steps": [ + { + "command": "apt install pkg1", + "description": "Install package 1", + "requires_root": True + } + ] + } + + template = Template.from_dict(data) + + self.assertEqual(template.name, "test") + self.assertEqual(template.description, "Test description") + self.assertEqual(len(template.packages), 2) + self.assertEqual(len(template.steps), 1) + self.assertEqual(template.steps[0].command, "apt install pkg1") + + def test_template_with_hardware_requirements(self): + """Test template with hardware requirements.""" + hw_req = HardwareRequirements( + min_ram_mb=4096, + min_cores=4, + requires_gpu=True, + gpu_vendor="NVIDIA" + ) + + template = Template( + name="gpu-template", + description="GPU template", + version="1.0.0", + hardware_requirements=hw_req + ) + + self.assertIsNotNone(template.hardware_requirements) + self.assertEqual(template.hardware_requirements.min_ram_mb, 4096) + self.assertEqual(template.hardware_requirements.requires_gpu, True) + + +class TestTemplateValidator(unittest.TestCase): + """Test TemplateValidator.""" + + def test_validate_valid_template(self): + """Test validating a valid template.""" + template = Template( + name="valid-template", + description="Valid template", + version="1.0.0", + packages=["pkg1"] + ) + + is_valid, errors = TemplateValidator.validate(template) + + self.assertTrue(is_valid) + self.assertEqual(len(errors), 0) + + def test_validate_missing_name(self): + """Test validating template with missing name.""" + template = Template( + name="", + description="Test", + version="1.0.0" + ) + + is_valid, errors = TemplateValidator.validate(template) + + self.assertFalse(is_valid) + self.assertIn("name is required", errors[0]) + + def test_validate_missing_packages_and_steps(self): + """Test validating template with no packages or steps.""" + template = Template( + name="empty-template", + description="Empty template", + version="1.0.0" + ) + + is_valid, errors = TemplateValidator.validate(template) + + self.assertFalse(is_valid) + self.assertIn("packages or steps", errors[0]) + + def test_validate_invalid_hardware_requirements(self): + """Test validating template with invalid hardware requirements.""" + hw_req = HardwareRequirements(min_ram_mb=-1) + template = Template( + name="invalid-hw", + description="Invalid hardware", + version="1.0.0", + packages=["pkg1"], + hardware_requirements=hw_req + ) + + is_valid, errors = TemplateValidator.validate(template) + + self.assertFalse(is_valid) + self.assertIn("min_ram_mb", errors[0]) + + +class TestTemplateManager(unittest.TestCase): + """Test TemplateManager.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + self.template_dir = Path(self.temp_dir) / "templates" + self.template_dir.mkdir() + + # Create a test template + self.test_template_data = { + "name": "test-template", + "description": "Test template", + "version": "1.0.0", + "packages": ["package1", "package2"], + "steps": [ + { + "command": "apt update", + "description": "Update packages", + "requires_root": True + } + ] + } + + # Write test template to file + template_file = self.template_dir / "test-template.yaml" + with open(template_file, 'w') as f: + yaml.dump(self.test_template_data, f) + + def tearDown(self): + """Clean up test fixtures.""" + shutil.rmtree(self.temp_dir) + + def test_load_template(self): + """Test loading a template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + template = manager.load_template("test-template") + + self.assertIsNotNone(template) + self.assertEqual(template.name, "test-template") + self.assertEqual(len(template.packages), 2) + + def test_load_nonexistent_template(self): + """Test loading a non-existent template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + template = manager.load_template("nonexistent") + + self.assertIsNone(template) + + def test_save_template(self): + """Test saving a template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="new-template", + description="New template", + version="1.0.0", + packages=["pkg1"] + ) + + template_path = manager.save_template(template, "new-template") + + self.assertTrue(template_path.exists()) + + # Verify it can be loaded + loaded = manager.load_template("new-template") + self.assertIsNotNone(loaded) + self.assertEqual(loaded.name, "new-template") + + def test_list_templates(self): + """Test listing templates.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + templates = manager.list_templates() + + self.assertIn("test-template", templates) + self.assertEqual(templates["test-template"]["name"], "test-template") + + def test_generate_commands_from_packages(self): + """Test generating commands from packages.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="test", + description="Test", + version="1.0.0", + packages=["package1", "package2"] + ) + + commands = manager.generate_commands(template) + + self.assertGreater(len(commands), 0) + self.assertTrue(any("package1" in cmd or "package2" in cmd for cmd in commands)) + + def test_generate_commands_from_steps(self): + """Test generating commands from steps.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="test", + description="Test", + version="1.0.0", + steps=[ + InstallationStep( + command="apt install pkg1", + description="Install pkg1" + ) + ] + ) + + commands = manager.generate_commands(template) + + self.assertEqual(len(commands), 1) + self.assertEqual(commands[0], "apt install pkg1") + + def test_import_template(self): + """Test importing a template from file.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + # Create a temporary template file + temp_file = Path(self.temp_dir) / "import-template.yaml" + with open(temp_file, 'w') as f: + yaml.dump(self.test_template_data, f) + + template = manager.import_template(str(temp_file)) + + self.assertIsNotNone(template) + self.assertEqual(template.name, "test-template") + + def test_export_template(self): + """Test exporting a template to file.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + export_path = Path(self.temp_dir) / "exported-template.yaml" + manager.export_template("test-template", str(export_path)) + + self.assertTrue(export_path.exists()) + + # Verify content + with open(export_path, 'r') as f: + data = yaml.safe_load(f) + self.assertEqual(data["name"], "test-template") + + +class TestHardwareCompatibility(unittest.TestCase): + """Test hardware compatibility checking.""" + + def setUp(self): + """Set up test fixtures.""" + self.manager = TemplateManager() + + def test_check_hardware_compatibility_no_requirements(self): + """Test checking compatibility with no requirements.""" + template = Template( + name="test", + description="Test", + version="1.0.0", + packages=["pkg1"] + ) + + is_compatible, warnings = self.manager.check_hardware_compatibility(template) + + self.assertTrue(is_compatible) + self.assertEqual(len(warnings), 0) + + def test_check_hardware_compatibility_with_requirements(self): + """Test checking compatibility with requirements.""" + hw_req = HardwareRequirements( + min_ram_mb=1024, + min_cores=2 + ) + + template = Template( + name="test", + description="Test", + version="1.0.0", + packages=["pkg1"], + hardware_requirements=hw_req + ) + + is_compatible, warnings = self.manager.check_hardware_compatibility(template) + + # Result depends on actual hardware, but should not crash + self.assertIsInstance(is_compatible, bool) + self.assertIsInstance(warnings, list) + + +class TestTemplateFormat(unittest.TestCase): + """Test template format handling.""" + + def setUp(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.mkdtemp() + self.template_dir = Path(self.temp_dir) / "templates" + self.template_dir.mkdir() + + def tearDown(self): + """Clean up test fixtures.""" + shutil.rmtree(self.temp_dir) + + def test_save_yaml_format(self): + """Test saving template in YAML format.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="yaml-test", + description="YAML test", + version="1.0.0", + packages=["pkg1"] + ) + + template_path = manager.save_template(template, "yaml-test", TemplateFormat.YAML) + + self.assertTrue(template_path.exists()) + self.assertEqual(template_path.suffix, ".yaml") + + # Verify it's valid YAML + with open(template_path, 'r') as f: + data = yaml.safe_load(f) + self.assertEqual(data["name"], "yaml-test") + + def test_save_json_format(self): + """Test saving template in JSON format.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template = Template( + name="json-test", + description="JSON test", + version="1.0.0", + packages=["pkg1"] + ) + + template_path = manager.save_template(template, "json-test", TemplateFormat.JSON) + + self.assertTrue(template_path.exists()) + self.assertEqual(template_path.suffix, ".json") + + # Verify it's valid JSON + with open(template_path, 'r') as f: + data = json.load(f) + self.assertEqual(data["name"], "json-test") + + def test_load_yaml_template(self): + """Test loading YAML template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template_data = { + "name": "yaml-load", + "description": "YAML load test", + "version": "1.0.0", + "packages": ["pkg1"] + } + + template_file = self.template_dir / "yaml-load.yaml" + with open(template_file, 'w') as f: + yaml.dump(template_data, f) + + template = manager.load_template("yaml-load") + + self.assertIsNotNone(template) + self.assertEqual(template.name, "yaml-load") + + def test_load_json_template(self): + """Test loading JSON template.""" + manager = TemplateManager(templates_dir=str(self.template_dir)) + + template_data = { + "name": "json-load", + "description": "JSON load test", + "version": "1.0.0", + "packages": ["pkg1"] + } + + template_file = self.template_dir / "json-load.json" + with open(template_file, 'w') as f: + json.dump(template_data, f) + + template = manager.load_template("json-load") + + self.assertIsNotNone(template) + self.assertEqual(template.name, "json-load") + + +if __name__ == '__main__': + unittest.main() + From 5f2dff371b53dbc81dede7c0fd6c7bf89b5dc27a Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 19 Nov 2025 06:12:39 +0500 Subject: [PATCH 02/16] fix: Address security and installation issues in template system --- cortex/cli.py | 14 ++++---------- cortex/templates.py | 30 +++++++++++++++++++----------- cortex/templates/lamp.yaml | 15 ++++++++++++--- cortex/templates/mean.yaml | 31 ++++++++++++++++++++----------- cortex/templates/mern.yaml | 31 ++++++++++++++++++++----------- cortex/templates/ml-ai.yaml | 6 +++--- 6 files changed, 78 insertions(+), 49 deletions(-) diff --git a/cortex/cli.py b/cortex/cli.py index d9fe5bd5..454b4e5b 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -298,7 +298,7 @@ def _install_from_template(self, template_name: str, execute: bool, dry_run: boo # Check hardware compatibility is_compatible, warnings = template_manager.check_hardware_compatibility(template) if warnings: - print(f"\n[WARNING] Hardware Compatibility Warnings:") + print("\n[WARNING] Hardware Compatibility Warnings:") for warning in warnings: print(f" - {warning}") if not is_compatible and not dry_run: @@ -391,22 +391,16 @@ def progress_callback(current, total, step): status = "[OK]" if passed else "[FAIL]" print(f" {status} {cmd}") - # Run post-install commands + # Run post-install commands once if template.post_install: self._print_status("[*]", "Running post-installation steps...") + print("\n[*] Post-installation information:") for cmd in template.post_install: subprocess.run(cmd, shell=True) self._print_success(f"{template.name} stack ready!") print(f"\nCompleted in {result.total_duration:.2f} seconds") - # Display post-install info - if template.post_install: - print("\n[*] Post-installation information:") - for cmd in template.post_install: - if cmd.startswith("echo"): - subprocess.run(cmd, shell=True) - # Record successful installation if install_id: history.update_installation(install_id, InstallationStatus.SUCCESS) @@ -445,7 +439,7 @@ def progress_callback(current, total, step): history.update_installation(install_id, InstallationStatus.FAILED, str(e)) self._print_error(str(e)) return 1 - except Exception as e: + except (RuntimeError, OSError, subprocess.SubprocessError) as e: if install_id: history.update_installation(install_id, InstallationStatus.FAILED, str(e)) self._print_error(f"Unexpected error: {str(e)}") diff --git a/cortex/templates.py b/cortex/templates.py index f4dd7f64..3ceab925 100644 --- a/cortex/templates.py +++ b/cortex/templates.py @@ -296,23 +296,31 @@ def list_templates(self) -> Dict[str, Dict[str, Any]]: """List all available templates.""" templates = {} - # List built-in templates + # List built-in templates (load directly from file to avoid user overrides) if self.templates_dir.exists(): for ext in [".yaml", ".yml", ".json"]: for template_file in self.templates_dir.glob(f"*{ext}"): name = template_file.stem + if name in templates: + # Skip duplicate names across extensions + continue try: - template = self.load_template(name) - if template: - templates[name] = { - "name": template.name, - "description": template.description, - "version": template.version, - "author": template.author, - "type": "built-in", - "path": str(template_file) - } + with open(template_file, 'r', encoding='utf-8') as f: + if template_file.suffix in ['.yaml', '.yml']: + data = yaml.safe_load(f) + else: + data = json.load(f) + template = Template.from_dict(data) + templates[name] = { + "name": template.name, + "description": template.description, + "version": template.version, + "author": template.author, + "type": "built-in", + "path": str(template_file) + } except Exception: + # Ignore malformed built-ins but continue listing others pass # List user templates diff --git a/cortex/templates/lamp.yaml b/cortex/templates/lamp.yaml index 5421437f..3e011e55 100644 --- a/cortex/templates/lamp.yaml +++ b/cortex/templates/lamp.yaml @@ -18,11 +18,17 @@ steps: - command: apt update description: Update package lists requires_root: true - - command: apt install -y apache2 mysql-server mysql-client php php-mysql php-mbstring php-xml php-curl libapache2-mod-php + - command: echo "mysql-server mysql-server/root_password password temp_password" | debconf-set-selections && echo "mysql-server mysql-server/root_password_again password temp_password" | debconf-set-selections + description: Pre-configure MySQL root password + requires_root: true + - command: DEBIAN_FRONTEND=noninteractive apt install -y apache2 mysql-server mysql-client php php-mysql php-mbstring php-xml php-curl libapache2-mod-php description: Install LAMP stack packages requires_root: true - - command: apt install -y phpmyadmin - description: Install phpMyAdmin + - command: echo "phpmyadmin phpmyadmin/dbconfig-install boolean true" | debconf-set-selections && echo "phpmyadmin phpmyadmin/reconfigure-webserver multiselect apache2" | debconf-set-selections && DEBIAN_FRONTEND=noninteractive apt install -y phpmyadmin + description: Install and configure phpMyAdmin for Apache + requires_root: true + - command: ln -sf /usr/share/phpmyadmin /var/www/html/phpmyadmin + description: Create phpMyAdmin symlink requires_root: true - command: systemctl start apache2 description: Start Apache web server @@ -48,8 +54,11 @@ hardware_requirements: post_install: - echo "LAMP stack installed successfully" + - echo "SECURITY: Run 'mysql_secure_installation' to secure MySQL" + - echo "SECURITY: Configure firewall rules for production use" - echo "Apache: http://localhost" - echo "phpMyAdmin: http://localhost/phpmyadmin" + - echo "SECURITY: Change default MySQL passwords before production use" verification_commands: - apache2 -v diff --git a/cortex/templates/mean.yaml b/cortex/templates/mean.yaml index 21bb9c68..0778e6fb 100644 --- a/cortex/templates/mean.yaml +++ b/cortex/templates/mean.yaml @@ -13,26 +13,35 @@ steps: - command: apt update description: Update package lists requires_root: true - - command: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - description: Add Node.js repository + - command: curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh && bash /tmp/nodesource_setup.sh && rm /tmp/nodesource_setup.sh + description: Download and add Node.js repository requires_root: true - command: apt install -y nodejs description: Install Node.js requires_root: true - command: npm install -g @angular/cli express-generator description: Install Angular CLI and Express generator globally - requires_root: false - - command: apt install -y mongodb - description: Install MongoDB requires_root: true - - command: systemctl start mongodb + - command: curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + description: Import MongoDB GPG key + requires_root: true + - command: echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list + description: Add MongoDB official apt repository + requires_root: true + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y mongodb-org + description: Install MongoDB from official repository + requires_root: true + - command: systemctl start mongod description: Start MongoDB service requires_root: true - rollback: systemctl stop mongodb - - command: systemctl enable mongodb + rollback: systemctl stop mongod + - command: systemctl enable mongod description: Enable MongoDB on boot requires_root: true - rollback: systemctl disable mongodb + rollback: systemctl disable mongod hardware_requirements: min_ram_mb: 2048 @@ -49,8 +58,8 @@ post_install: verification_commands: - node --version - npm --version - - mongod --version - - systemctl is-active mongodb + - mongosh --version || mongo --version + - systemctl is-active mongod - ng version metadata: diff --git a/cortex/templates/mern.yaml b/cortex/templates/mern.yaml index 953606ac..59d84e8c 100644 --- a/cortex/templates/mern.yaml +++ b/cortex/templates/mern.yaml @@ -13,26 +13,35 @@ steps: - command: apt update description: Update package lists requires_root: true - - command: curl -fsSL https://deb.nodesource.com/setup_20.x | bash - - description: Add Node.js repository + - command: curl -fsSL https://deb.nodesource.com/setup_20.x -o /tmp/nodesource_setup.sh && bash /tmp/nodesource_setup.sh && rm /tmp/nodesource_setup.sh + description: Download and add Node.js repository requires_root: true - command: apt install -y nodejs description: Install Node.js requires_root: true - command: npm install -g create-react-app express-generator description: Install React and Express generators globally - requires_root: false - - command: apt install -y mongodb - description: Install MongoDB requires_root: true - - command: systemctl start mongodb + - command: curl -fsSL https://www.mongodb.org/static/pgp/server-8.0.asc | gpg -o /usr/share/keyrings/mongodb-server-8.0.gpg --dearmor + description: Import MongoDB GPG key + requires_root: true + - command: echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-8.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/8.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-8.0.list + description: Add MongoDB official apt repository + requires_root: true + - command: apt update + description: Update package lists + requires_root: true + - command: apt install -y mongodb-org + description: Install MongoDB from official repository + requires_root: true + - command: systemctl start mongod description: Start MongoDB service requires_root: true - rollback: systemctl stop mongodb - - command: systemctl enable mongodb + rollback: systemctl stop mongod + - command: systemctl enable mongod description: Enable MongoDB on boot requires_root: true - rollback: systemctl disable mongodb + rollback: systemctl disable mongod hardware_requirements: min_ram_mb: 2048 @@ -49,8 +58,8 @@ post_install: verification_commands: - node --version - npm --version - - mongod --version - - systemctl is-active mongodb + - mongosh --version || mongo --version + - systemctl is-active mongod - npx create-react-app --version metadata: diff --git a/cortex/templates/ml-ai.yaml b/cortex/templates/ml-ai.yaml index 58809611..681385a0 100644 --- a/cortex/templates/ml-ai.yaml +++ b/cortex/templates/ml-ai.yaml @@ -1,5 +1,5 @@ name: ML/AI Stack -description: Machine Learning and Artificial Intelligence development stack with Python, TensorFlow, PyTorch, and Jupyter +description: Machine Learning and Artificial Intelligence development stack with Python, TensorFlow, PyTorch, and Jupyter. GPU recommended for deep learning workloads. version: 1.0.0 author: Cortex Linux packages: @@ -33,9 +33,9 @@ steps: requires_root: false hardware_requirements: - min_ram_mb: 4096 + min_ram_mb: 8192 min_cores: 4 - min_storage_mb: 20480 + min_storage_mb: 30720 requires_gpu: false requires_cuda: false From ea9c9fa7a12b695c7295839908514b13966a58f5 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 19 Nov 2025 06:31:47 +0500 Subject: [PATCH 03/16] fix: Security and installation fixes for template system --- cortex/cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cortex/cli.py b/cortex/cli.py index 454b4e5b..be881485 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -305,10 +305,12 @@ def _install_from_template(self, template_name: str, execute: bool, dry_run: boo try: response = input("\n[WARNING] Hardware requirements not met. Continue anyway? (y/N): ") if response.lower() != 'y': + print("\n[INFO] Installation aborted by user") return 1 except (EOFError, KeyboardInterrupt): # Non-interactive environment or user cancelled - print("\n[INFO] Skipping hardware check prompt (non-interactive mode)") + print("\n[ERROR] Aborting install: cannot prompt for hardware confirmation in non-interactive mode") + print(" Use --dry-run to preview commands, or ensure hardware requirements are met") return 1 # Generate commands From bfaa85b372ba200097e7c18dcec34be0646e2e4b Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 19 Nov 2025 06:51:16 +0500 Subject: [PATCH 04/16] feat: Add installation templates system for common development stacks --- cortex/cli.py | 27 +++++++++++--------- cortex/templates.py | 62 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 12 deletions(-) diff --git a/cortex/cli.py b/cortex/cli.py index be881485..97613c3c 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -511,11 +511,15 @@ def template_create(self, name: str, interactive: bool = True): min_storage = input(" Minimum storage (MB, optional): ").strip() if min_ram or min_cores or min_storage: - hw_req = HardwareRequirements( - min_ram_mb=int(min_ram) if min_ram else None, - min_cores=int(min_cores) if min_cores else None, - min_storage_mb=int(min_storage) if min_storage else None - ) + try: + hw_req = HardwareRequirements( + min_ram_mb=int(min_ram) if min_ram else None, + min_cores=int(min_cores) if min_cores else None, + min_storage_mb=int(min_storage) if min_storage else None + ) + except ValueError: + self._print_error("Hardware requirements must be numeric values") + return 1 template.hardware_requirements = hw_req # Save template @@ -595,8 +599,9 @@ def main(): # Install command install_parser = subparsers.add_parser('install', help='Install software using natural language or template') - install_parser.add_argument('software', type=str, nargs='?', help='Software to install (natural language)') - install_parser.add_argument('--template', type=str, help='Install from template (e.g., lamp, mean, mern)') + install_group = install_parser.add_mutually_exclusive_group(required=True) + install_group.add_argument('software', type=str, nargs='?', help='Software to install (natural language)') + install_group.add_argument('--template', type=str, help='Install from template (e.g., lamp, mean, mern)') install_parser.add_argument('--execute', action='store_true', help='Execute the generated commands') install_parser.add_argument('--dry-run', action='store_true', help='Show commands without executing') @@ -617,7 +622,7 @@ def main(): template_subparsers = template_parser.add_subparsers(dest='template_action', help='Template actions') # Template list - template_list_parser = template_subparsers.add_parser('list', help='List all available templates') + template_subparsers.add_parser('list', help='List all available templates') # Template create template_create_parser = template_subparsers.add_parser('create', help='Create a new template') @@ -646,11 +651,9 @@ def main(): if args.command == 'install': if args.template: return cli.install("", execute=args.execute, dry_run=args.dry_run, template=args.template) - elif args.software: - return cli.install(args.software, execute=args.execute, dry_run=args.dry_run) else: - install_parser.print_help() - return 1 + # software is guaranteed to be set due to mutually_exclusive_group(required=True) + return cli.install(args.software, execute=args.execute, dry_run=args.dry_run) elif args.command == 'history': return cli.history(limit=args.limit, status=args.status, show_id=args.show_id) elif args.command == 'rollback': diff --git a/cortex/templates.py b/cortex/templates.py index 3ceab925..1943fa2e 100644 --- a/cortex/templates.py +++ b/cortex/templates.py @@ -154,6 +154,63 @@ class TemplateValidator: REQUIRED_FIELDS = ["name", "description", "version"] REQUIRED_STEP_FIELDS = ["command", "description"] + # Allowed post_install commands (whitelist for security) + ALLOWED_POST_INSTALL_COMMANDS = { + 'echo', # Safe echo commands + } + + # Dangerous shell metacharacters that should be rejected + DANGEROUS_SHELL_CHARS = [';', '|', '&', '>', '<', '`', '\\'] + + @staticmethod + def _validate_post_install_commands(post_install: List[str]) -> List[str]: + """ + Validate post_install commands for security. + + Returns: + List of validation errors + """ + errors = [] + + for i, cmd in enumerate(post_install): + if not cmd or not cmd.strip(): + continue + + cmd_stripped = cmd.strip() + + # Check for dangerous shell metacharacters + for char in TemplateValidator.DANGEROUS_SHELL_CHARS: + if char in cmd_stripped: + errors.append( + f"post_install[{i}]: Contains dangerous shell character '{char}'. " + "Only safe commands like 'echo' are allowed." + ) + break + + # Check for command substitution patterns + if '$(' in cmd_stripped or '`' in cmd_stripped: + # Allow $(...) in echo commands for version checks (built-in templates use this) + if not cmd_stripped.startswith('echo '): + errors.append( + f"post_install[{i}]: Command substitution only allowed in 'echo' commands" + ) + + # Check for wildcards/globs + if '*' in cmd_stripped or '?' in cmd_stripped: + if not cmd_stripped.startswith('echo '): + errors.append( + f"post_install[{i}]: Wildcards only allowed in 'echo' commands" + ) + + # Whitelist check - only allow echo commands + if not cmd_stripped.startswith('echo '): + errors.append( + f"post_install[{i}]: Only 'echo' commands are allowed in post_install. " + f"Found: {cmd_stripped[:50]}" + ) + + return errors + @staticmethod def validate(template: Template) -> Tuple[bool, List[str]]: """ @@ -195,6 +252,11 @@ def validate(template: Template) -> Tuple[bool, List[str]]: if hw.requires_cuda and not hw.requires_gpu: errors.append("requires_cuda is true but requires_gpu is false") + # Validate post_install commands for security + if template.post_install: + post_install_errors = TemplateValidator._validate_post_install_commands(template.post_install) + errors.extend(post_install_errors) + return len(errors) == 0, errors From 0698ae0d628c610f7794fdbbf8d6d25585cce19b Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Sat, 29 Nov 2025 00:17:28 +0500 Subject: [PATCH 05/16] fix unit tests --- requirements.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/requirements.txt b/requirements.txt index 25a4cd2c..f2ad73b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,8 @@ anthropic>=0.18.0 openai>=1.0.0 +# YAML parsing for template system +PyYAML>=6.0 + # Type hints for older Python versions typing-extensions>=4.0.0 From 8eb269fff7943beb192630da080173f0413207b7 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Sat, 20 Dec 2025 13:03:13 +0500 Subject: [PATCH 06/16] refomattrf files --- cortex/cli.py | 192 ++++++++++++++++----------- cortex/templates.py | 264 ++++++++++++++++++------------------- tests/test_templates.py | 280 +++++++++++++++++----------------------- 3 files changed, 364 insertions(+), 372 deletions(-) diff --git a/cortex/cli.py b/cortex/cli.py index 14ab6aef..b1be2671 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -286,7 +286,13 @@ def doctor(self): doctor = SystemDoctor() return doctor.run_checks() - def install(self, software: str, execute: bool = False, dry_run: bool = False, template: Optional[str] = None): + def install( + self, + software: str, + execute: bool = False, + dry_run: bool = False, + template: Optional[str] = None, + ): # Validate input first (only if not using template) if not template: is_valid, error = validate_install_request(software) @@ -367,6 +373,7 @@ def progress_callback(current, total, step): status_emoji = "āŒ" print(f"\n[{current}/{total}] {status_emoji} {step.description}") print(f" Command: {step.command}") + print("\nExecuting commands...") coordinator = InstallationCoordinator( commands=commands, @@ -523,19 +530,19 @@ def rollback(self, install_id: str, dry_run: bool = False): except Exception as e: self._print_error(f"Rollback failed: {str(e)}") return 1 - + def _install_from_template(self, template_name: str, execute: bool, dry_run: bool): """Install from a template.""" history = InstallationHistory() install_id = None start_time = datetime.now() - + try: template_manager = TemplateManager() - + self._print_status("[*]", f"Loading template: {template_name}...") template = template_manager.load_template(template_name) - + if not template: self._print_error(f"Template '{template_name}' not found") self._print_status("[*]", "Available templates:") @@ -543,14 +550,14 @@ def _install_from_template(self, template_name: str, execute: bool, dry_run: boo for name, info in templates.items(): print(f" - {name}: {info['description']}") return 1 - + # Display template info print(f"\n{template.name} Template:") print(f" {template.description}") print(f"\n Packages:") for pkg in template.packages: print(f" - {pkg}") - + # Check hardware compatibility is_compatible, warnings = template_manager.check_hardware_compatibility(template) if warnings: @@ -559,47 +566,54 @@ def _install_from_template(self, template_name: str, execute: bool, dry_run: boo print(f" - {warning}") if not is_compatible and not dry_run: try: - response = input("\n[WARNING] Hardware requirements not met. Continue anyway? (y/N): ") - if response.lower() != 'y': + response = input( + "\n[WARNING] Hardware requirements not met. Continue anyway? (y/N): " + ) + if response.lower() != "y": print("\n[INFO] Installation aborted by user") return 1 except (EOFError, KeyboardInterrupt): # Non-interactive environment or user cancelled - print("\n[ERROR] Aborting install: cannot prompt for hardware confirmation in non-interactive mode") - print(" Use --dry-run to preview commands, or ensure hardware requirements are met") + print( + "\n[ERROR] Aborting install: cannot prompt for hardware confirmation in non-interactive mode" + ) + print( + " Use --dry-run to preview commands, or ensure hardware requirements are met" + ) return 1 - + # Generate commands self._print_status("[*]", "Generating installation commands...") commands = template_manager.generate_commands(template) - + if not commands: self._print_error("No commands generated from template") return 1 - + # Extract packages for tracking - packages = template.packages if template.packages else history._extract_packages_from_commands(commands) - + packages = ( + template.packages + if template.packages + else history._extract_packages_from_commands(commands) + ) + # Record installation start if execute or dry_run: install_id = history.record_installation( - InstallationType.INSTALL, - packages, - commands, - start_time + InstallationType.INSTALL, packages, commands, start_time ) - + print(f"\n[*] Installing {len(packages)} packages...") print("\nGenerated commands:") for i, cmd in enumerate(commands, 1): print(f" {i}. {cmd}") - + if dry_run: print("\n(Dry run mode - commands not executed)") if install_id: history.update_installation(install_id, InstallationStatus.SUCCESS) return 0 - + if execute: # Convert template steps to coordinator format if available if template.steps: @@ -607,16 +621,15 @@ def _install_from_template(self, template_name: str, execute: bool, dry_run: boo { "command": step.command, "description": step.description, - "rollback": step.rollback + "rollback": step.rollback, } for step in template.steps ] coordinator = InstallationCoordinator.from_plan( - plan, - timeout=300, - stop_on_error=True + plan, timeout=300, stop_on_error=True ) else: + def progress_callback(current, total, step): status_emoji = "ā³" if step.status == StepStatus.SUCCESS: @@ -625,57 +638,57 @@ def progress_callback(current, total, step): status_emoji = "āŒ" print(f"\n[{current}/{total}] {status_emoji} {step.description}") print(f" Command: {step.command}") - + coordinator = InstallationCoordinator( commands=commands, descriptions=[f"Step {i+1}" for i in range(len(commands))], timeout=300, stop_on_error=True, - progress_callback=progress_callback + progress_callback=progress_callback, ) - + print("\nExecuting commands...") result = coordinator.execute() - + if result.success: # Run verification commands if available if template.verification_commands: self._print_status("[*]", "Verifying installation...") - verify_results = coordinator.verify_installation(template.verification_commands) + verify_results = coordinator.verify_installation( + template.verification_commands + ) all_passed = all(verify_results.values()) if not all_passed: print("\n[WARNING] Some verification checks failed:") for cmd, passed in verify_results.items(): status = "[OK]" if passed else "[FAIL]" print(f" {status} {cmd}") - + # Run post-install commands once if template.post_install: self._print_status("[*]", "Running post-installation steps...") print("\n[*] Post-installation information:") for cmd in template.post_install: subprocess.run(cmd, shell=True) - + self._print_success(f"{template.name} stack ready!") print(f"\nCompleted in {result.total_duration:.2f} seconds") - + # Record successful installation if install_id: history.update_installation(install_id, InstallationStatus.SUCCESS) print(f"\n[*] Installation recorded (ID: {install_id})") print(f" To rollback: cortex rollback {install_id}") - + return 0 else: # Record failed installation if install_id: error_msg = result.error_message or "Installation failed" history.update_installation( - install_id, - InstallationStatus.FAILED, - error_msg + install_id, InstallationStatus.FAILED, error_msg ) - + if result.failed_step is not None: self._print_error(f"Installation failed at step {result.failed_step + 1}") else: @@ -689,9 +702,9 @@ def progress_callback(current, total, step): else: print("\nTo execute these commands, run with --execute flag") print(f"Example: cortex install --template {template_name} --execute") - + return 0 - + except ValueError as e: if install_id: history.update_installation(install_id, InstallationStatus.FAILED, str(e)) @@ -702,46 +715,50 @@ def progress_callback(current, total, step): history.update_installation(install_id, InstallationStatus.FAILED, str(e)) self._print_error(f"Unexpected error: {str(e)}") return 1 - + def template_list(self): """List all available templates.""" try: template_manager = TemplateManager() templates = template_manager.list_templates() - + if not templates: print("No templates found.") return 0 - + print("\nAvailable Templates:") print("=" * 80) print(f"{'Name':<20} {'Version':<12} {'Type':<12} {'Description':<35}") print("=" * 80) - + for name, info in sorted(templates.items()): - desc = info['description'][:33] + "..." if len(info['description']) > 35 else info['description'] + desc = ( + info["description"][:33] + "..." + if len(info["description"]) > 35 + else info["description"] + ) print(f"{name:<20} {info['version']:<12} {info['type']:<12} {desc:<35}") - + print(f"\nTotal: {len(templates)} templates") return 0 except Exception as e: self._print_error(f"Failed to list templates: {str(e)}") return 1 - + def template_create(self, name: str, interactive: bool = True): """Create a new template interactively.""" try: print(f"\n[*] Creating template: {name}") - + if interactive: description = input("Description: ").strip() if not description: self._print_error("Description is required") return 1 - + version = input("Version (default: 1.0.0): ").strip() or "1.0.0" author = input("Author (optional): ").strip() or None - + print("\nEnter packages (one per line, empty line to finish):") packages = [] while True: @@ -749,74 +766,77 @@ def template_create(self, name: str, interactive: bool = True): if not pkg: break packages.append(pkg) - + # Create template from cortex.templates import Template, HardwareRequirements + template = Template( name=name, description=description, version=version, author=author, - packages=packages + packages=packages, ) - + # Ask about hardware requirements print("\nHardware Requirements (optional):") min_ram = input(" Minimum RAM (MB, optional): ").strip() min_cores = input(" Minimum CPU cores (optional): ").strip() min_storage = input(" Minimum storage (MB, optional): ").strip() - + if min_ram or min_cores or min_storage: try: hw_req = HardwareRequirements( min_ram_mb=int(min_ram) if min_ram else None, min_cores=int(min_cores) if min_cores else None, - min_storage_mb=int(min_storage) if min_storage else None + min_storage_mb=int(min_storage) if min_storage else None, ) except ValueError: self._print_error("Hardware requirements must be numeric values") return 1 template.hardware_requirements = hw_req - + # Save template template_manager = TemplateManager() template_path = template_manager.save_template(template, name) - + self._print_success(f"Template '{name}' created successfully!") print(f" Saved to: {template_path}") return 0 else: self._print_error("Non-interactive template creation not yet supported") return 1 - + except Exception as e: self._print_error(f"Failed to create template: {str(e)}") return 1 - + def template_import(self, file_path: str, name: Optional[str] = None): """Import a template from a file.""" try: template_manager = TemplateManager() template = template_manager.import_template(file_path, name) - + # Save to user templates save_name = name or template.name template_path = template_manager.save_template(template, save_name) - + self._print_success(f"Template '{save_name}' imported successfully!") print(f" Saved to: {template_path}") return 0 except Exception as e: self._print_error(f"Failed to import template: {str(e)}") return 1 - + def template_export(self, name: str, file_path: str, format: str = "yaml"): """Export a template to a file.""" try: template_manager = TemplateManager() - template_format = TemplateFormat.YAML if format.lower() == "yaml" else TemplateFormat.JSON + template_format = ( + TemplateFormat.YAML if format.lower() == "yaml" else TemplateFormat.JSON + ) export_path = template_manager.export_template(name, file_path, template_format) - + self._print_success(f"Template '{name}' exported successfully!") print(f" Saved to: {export_path}") return 0 @@ -1030,7 +1050,7 @@ def main(): Environment Variables: OPENAI_API_KEY OpenAI API key for GPT-4 ANTHROPIC_API_KEY Anthropic API key for Claude - """ + """, ) # Global flags @@ -1055,12 +1075,22 @@ def main(): doctor_parser = subparsers.add_parser("doctor", help="Run system health check") # Install command - install_parser = subparsers.add_parser("install", help="Install software using natural language or template") + install_parser = subparsers.add_parser( + "install", help="Install software using natural language or template" + ) install_group = install_parser.add_mutually_exclusive_group(required=True) - install_group.add_argument("software", type=str, nargs="?", help="Software to install (natural language)") - install_group.add_argument("--template", type=str, help="Install from template (e.g., lamp, mean, mern)") - install_parser.add_argument("--execute", action="store_true", help="Execute the generated commands") - install_parser.add_argument("--dry-run", action="store_true", help="Show commands without executing") + install_group.add_argument( + "software", type=str, nargs="?", help="Software to install (natural language)" + ) + install_group.add_argument( + "--template", type=str, help="Install from template (e.g., lamp, mean, mern)" + ) + install_parser.add_argument( + "--execute", action="store_true", help="Execute the generated commands" + ) + install_parser.add_argument( + "--dry-run", action="store_true", help="Show commands without executing" + ) # History command history_parser = subparsers.add_parser("history", help="View history") @@ -1071,7 +1101,9 @@ def main(): # Rollback command rollback_parser = subparsers.add_parser("rollback", help="Rollback an installation") rollback_parser.add_argument("id", help="Installation ID to rollback") - rollback_parser.add_argument("--dry-run", action="store_true", help="Show rollback actions without executing") + rollback_parser.add_argument( + "--dry-run", action="store_true", help="Show rollback actions without executing" + ) # Preferences commands check_pref_parser = subparsers.add_parser("check-pref", help="Check preferences") @@ -1105,18 +1137,20 @@ def main(): template_parser = subparsers.add_parser("template", help="Manage installation templates") template_subs = template_parser.add_subparsers(dest="template_action", help="Template actions") template_subs.add_parser("list", help="List all available templates") - + template_create_parser = template_subs.add_parser("create", help="Create a new template") template_create_parser.add_argument("name", help="Template name") - + template_import_parser = template_subs.add_parser("import", help="Import a template from file") template_import_parser.add_argument("file_path", help="Path to template file") template_import_parser.add_argument("--name", help="Override template name") - + template_export_parser = template_subs.add_parser("export", help="Export a template to file") template_export_parser.add_argument("name", help="Template name") template_export_parser.add_argument("file_path", help="Output file path") - template_export_parser.add_argument("--format", choices=["yaml", "json"], default="yaml", help="Export format") + template_export_parser.add_argument( + "--format", choices=["yaml", "json"], default="yaml", help="Export format" + ) # Stack command stack_parser = subparsers.add_parser("stack", help="Manage pre-built package stacks") @@ -1152,7 +1186,9 @@ def main(): return cli.status() elif args.command == "install": if args.template: - return cli.install("", execute=args.execute, dry_run=args.dry_run, template=args.template) + return cli.install( + "", execute=args.execute, dry_run=args.dry_run, template=args.template + ) else: # software is guaranteed to be set due to mutually_exclusive_group(required=True) return cli.install(args.software, execute=args.execute, dry_run=args.dry_run) diff --git a/cortex/templates.py b/cortex/templates.py index 1943fa2e..2881b850 100644 --- a/cortex/templates.py +++ b/cortex/templates.py @@ -16,7 +16,7 @@ from enum import Enum # Add parent directory to path for imports -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from src.hwprofiler import HardwareProfiler from cortex.packages import PackageManager, PackageManagerType @@ -24,6 +24,7 @@ class TemplateFormat(Enum): """Supported template formats.""" + YAML = "yaml" JSON = "json" @@ -31,6 +32,7 @@ class TemplateFormat(Enum): @dataclass class HardwareRequirements: """Hardware requirements for a template.""" + min_ram_mb: Optional[int] = None min_cores: Optional[int] = None min_storage_mb: Optional[int] = None @@ -43,6 +45,7 @@ class HardwareRequirements: @dataclass class InstallationStep: """A single installation step in a template.""" + command: str description: str rollback: Optional[str] = None @@ -53,6 +56,7 @@ class InstallationStep: @dataclass class Template: """Represents an installation template.""" + name: str description: str version: str @@ -63,7 +67,7 @@ class Template: post_install: List[str] = field(default_factory=list) verification_commands: List[str] = field(default_factory=list) metadata: Dict[str, Any] = field(default_factory=dict) - + def to_dict(self) -> Dict[str, Any]: """Convert template to dictionary.""" result = { @@ -73,12 +77,12 @@ def to_dict(self) -> Dict[str, Any]: "packages": self.packages, "post_install": self.post_install, "verification_commands": self.verification_commands, - "metadata": self.metadata + "metadata": self.metadata, } - + if self.author: result["author"] = self.author - + if self.steps: result["steps"] = [ { @@ -86,11 +90,11 @@ def to_dict(self) -> Dict[str, Any]: "description": step.description, "rollback": step.rollback, "verify": step.verify, - "requires_root": step.requires_root + "requires_root": step.requires_root, } for step in self.steps ] - + if self.hardware_requirements: hw = self.hardware_requirements result["hardware_requirements"] = { @@ -100,11 +104,11 @@ def to_dict(self) -> Dict[str, Any]: "requires_gpu": hw.requires_gpu, "gpu_vendor": hw.gpu_vendor, "requires_cuda": hw.requires_cuda, - "min_cuda_version": hw.min_cuda_version + "min_cuda_version": hw.min_cuda_version, } - + return result - + @classmethod def from_dict(cls, data: Dict[str, Any]) -> "Template": """Create template from dictionary.""" @@ -119,21 +123,23 @@ def from_dict(cls, data: Dict[str, Any]) -> "Template": requires_gpu=hw_data.get("requires_gpu", False), gpu_vendor=hw_data.get("gpu_vendor"), requires_cuda=hw_data.get("requires_cuda", False), - min_cuda_version=hw_data.get("min_cuda_version") + min_cuda_version=hw_data.get("min_cuda_version"), ) - + # Parse installation steps steps = [] if "steps" in data: for step_data in data["steps"]: - steps.append(InstallationStep( - command=step_data["command"], - description=step_data.get("description", ""), - rollback=step_data.get("rollback"), - verify=step_data.get("verify"), - requires_root=step_data.get("requires_root", True) - )) - + steps.append( + InstallationStep( + command=step_data["command"], + description=step_data.get("description", ""), + rollback=step_data.get("rollback"), + verify=step_data.get("verify"), + requires_root=step_data.get("requires_root", True), + ) + ) + return cls( name=data["name"], description=data["description"], @@ -144,40 +150,40 @@ def from_dict(cls, data: Dict[str, Any]) -> "Template": hardware_requirements=hw_req, post_install=data.get("post_install", []), verification_commands=data.get("verification_commands", []), - metadata=data.get("metadata", {}) + metadata=data.get("metadata", {}), ) class TemplateValidator: """Validates template structure and content.""" - + REQUIRED_FIELDS = ["name", "description", "version"] REQUIRED_STEP_FIELDS = ["command", "description"] - + # Allowed post_install commands (whitelist for security) ALLOWED_POST_INSTALL_COMMANDS = { - 'echo', # Safe echo commands + "echo", # Safe echo commands } - + # Dangerous shell metacharacters that should be rejected - DANGEROUS_SHELL_CHARS = [';', '|', '&', '>', '<', '`', '\\'] - + DANGEROUS_SHELL_CHARS = [";", "|", "&", ">", "<", "`", "\\"] + @staticmethod def _validate_post_install_commands(post_install: List[str]) -> List[str]: """ Validate post_install commands for security. - + Returns: List of validation errors """ errors = [] - + for i, cmd in enumerate(post_install): if not cmd or not cmd.strip(): continue - + cmd_stripped = cmd.strip() - + # Check for dangerous shell metacharacters for char in TemplateValidator.DANGEROUS_SHELL_CHARS: if char in cmd_stripped: @@ -186,41 +192,39 @@ def _validate_post_install_commands(post_install: List[str]) -> List[str]: "Only safe commands like 'echo' are allowed." ) break - + # Check for command substitution patterns - if '$(' in cmd_stripped or '`' in cmd_stripped: + if "$(" in cmd_stripped or "`" in cmd_stripped: # Allow $(...) in echo commands for version checks (built-in templates use this) - if not cmd_stripped.startswith('echo '): + if not cmd_stripped.startswith("echo "): errors.append( f"post_install[{i}]: Command substitution only allowed in 'echo' commands" ) - + # Check for wildcards/globs - if '*' in cmd_stripped or '?' in cmd_stripped: - if not cmd_stripped.startswith('echo '): - errors.append( - f"post_install[{i}]: Wildcards only allowed in 'echo' commands" - ) - + if "*" in cmd_stripped or "?" in cmd_stripped: + if not cmd_stripped.startswith("echo "): + errors.append(f"post_install[{i}]: Wildcards only allowed in 'echo' commands") + # Whitelist check - only allow echo commands - if not cmd_stripped.startswith('echo '): + if not cmd_stripped.startswith("echo "): errors.append( f"post_install[{i}]: Only 'echo' commands are allowed in post_install. " f"Found: {cmd_stripped[:50]}" ) - + return errors - + @staticmethod def validate(template: Template) -> Tuple[bool, List[str]]: """ Validate a template. - + Returns: Tuple of (is_valid, list_of_errors) """ errors = [] - + # Check required fields if not template.name: errors.append("Template name is required") @@ -228,18 +232,18 @@ def validate(template: Template) -> Tuple[bool, List[str]]: errors.append("Template description is required") if not template.version: errors.append("Template version is required") - + # Validate steps for i, step in enumerate(template.steps): if not step.command: errors.append(f"Step {i+1}: command is required") if not step.description: errors.append(f"Step {i+1}: description is required") - + # Validate packages list if not template.packages and not template.steps: errors.append("Template must have either packages or steps defined") - + # Validate hardware requirements if template.hardware_requirements: hw = template.hardware_requirements @@ -251,22 +255,24 @@ def validate(template: Template) -> Tuple[bool, List[str]]: errors.append("min_storage_mb must be non-negative") if hw.requires_cuda and not hw.requires_gpu: errors.append("requires_cuda is true but requires_gpu is false") - + # Validate post_install commands for security if template.post_install: - post_install_errors = TemplateValidator._validate_post_install_commands(template.post_install) + post_install_errors = TemplateValidator._validate_post_install_commands( + template.post_install + ) errors.extend(post_install_errors) - + return len(errors) == 0, errors class TemplateManager: """Manages installation templates.""" - + def __init__(self, templates_dir: Optional[str] = None): """ Initialize template manager. - + Args: templates_dir: Directory containing templates (defaults to built-in templates) """ @@ -276,14 +282,14 @@ def __init__(self, templates_dir: Optional[str] = None): # Default to built-in templates directory base_dir = Path(__file__).parent self.templates_dir = base_dir / "templates" - + self.user_templates_dir = Path.home() / ".cortex" / "templates" self.user_templates_dir.mkdir(parents=True, exist_ok=True) - + self._templates_cache: Dict[str, Template] = {} self._hardware_profiler = HardwareProfiler() self._package_manager = PackageManager() - + def _get_template_path(self, name: str) -> Optional[Path]: """Find template file by name.""" # Check user templates first @@ -291,47 +297,51 @@ def _get_template_path(self, name: str) -> Optional[Path]: user_path = self.user_templates_dir / f"{name}{ext}" if user_path.exists(): return user_path - + # Check built-in templates for ext in [".yaml", ".yml", ".json"]: builtin_path = self.templates_dir / f"{name}{ext}" if builtin_path.exists(): return builtin_path - + return None - + def load_template(self, name: str) -> Optional[Template]: """Load a template by name.""" if name in self._templates_cache: return self._templates_cache[name] - + template_path = self._get_template_path(name) if not template_path: return None - + try: - with open(template_path, 'r', encoding='utf-8') as f: - if template_path.suffix in ['.yaml', '.yml']: + with open(template_path, "r", encoding="utf-8") as f: + if template_path.suffix in [".yaml", ".yml"]: data = yaml.safe_load(f) else: data = json.load(f) - + template = Template.from_dict(data) self._templates_cache[name] = template return template except Exception as e: raise ValueError(f"Failed to load template {name}: {str(e)}") - - def save_template(self, template: Template, name: Optional[str] = None, - format: TemplateFormat = TemplateFormat.YAML) -> Path: + + def save_template( + self, + template: Template, + name: Optional[str] = None, + format: TemplateFormat = TemplateFormat.YAML, + ) -> Path: """ Save a template to user templates directory. - + Args: template: Template to save name: Template name (defaults to template.name) format: File format (YAML or JSON) - + Returns: Path to saved template file """ @@ -339,25 +349,25 @@ def save_template(self, template: Template, name: Optional[str] = None, is_valid, errors = TemplateValidator.validate(template) if not is_valid: raise ValueError(f"Template validation failed: {', '.join(errors)}") - + template_name = name or template.name ext = ".yaml" if format == TemplateFormat.YAML else ".json" template_path = self.user_templates_dir / f"{template_name}{ext}" - + data = template.to_dict() - - with open(template_path, 'w', encoding='utf-8') as f: + + with open(template_path, "w", encoding="utf-8") as f: if format == TemplateFormat.YAML: yaml.dump(data, f, default_flow_style=False, sort_keys=False) else: json.dump(data, f, indent=2) - + return template_path - + def list_templates(self) -> Dict[str, Dict[str, Any]]: """List all available templates.""" templates = {} - + # List built-in templates (load directly from file to avoid user overrides) if self.templates_dir.exists(): for ext in [".yaml", ".yml", ".json"]: @@ -367,8 +377,8 @@ def list_templates(self) -> Dict[str, Dict[str, Any]]: # Skip duplicate names across extensions continue try: - with open(template_file, 'r', encoding='utf-8') as f: - if template_file.suffix in ['.yaml', '.yml']: + with open(template_file, "r", encoding="utf-8") as f: + if template_file.suffix in [".yaml", ".yml"]: data = yaml.safe_load(f) else: data = json.load(f) @@ -379,12 +389,12 @@ def list_templates(self) -> Dict[str, Dict[str, Any]]: "version": template.version, "author": template.author, "type": "built-in", - "path": str(template_file) + "path": str(template_file), } except Exception: # Ignore malformed built-ins but continue listing others pass - + # List user templates if self.user_templates_dir.exists(): for ext in [".yaml", ".yml", ".json"]: @@ -400,27 +410,27 @@ def list_templates(self) -> Dict[str, Dict[str, Any]]: "version": template.version, "author": template.author, "type": "user", - "path": str(template_file) + "path": str(template_file), } except Exception: pass - + return templates - + def check_hardware_compatibility(self, template: Template) -> Tuple[bool, List[str]]: """ Check if current hardware meets template requirements. - + Returns: Tuple of (is_compatible, list_of_warnings) """ if not template.hardware_requirements: return True, [] - + hw_profile = self._hardware_profiler.profile() hw_req = template.hardware_requirements warnings = [] - + # Check RAM if hw_req.min_ram_mb: available_ram = hw_profile.get("ram", 0) @@ -429,7 +439,7 @@ def check_hardware_compatibility(self, template: Template) -> Tuple[bool, List[s f"Insufficient RAM: {available_ram}MB available, " f"{hw_req.min_ram_mb}MB required" ) - + # Check CPU cores if hw_req.min_cores: available_cores = hw_profile.get("cpu", {}).get("cores", 0) @@ -438,32 +448,26 @@ def check_hardware_compatibility(self, template: Template) -> Tuple[bool, List[s f"Insufficient CPU cores: {available_cores} available, " f"{hw_req.min_cores} required" ) - + # Check storage if hw_req.min_storage_mb: - total_storage = sum( - s.get("size", 0) for s in hw_profile.get("storage", []) - ) + total_storage = sum(s.get("size", 0) for s in hw_profile.get("storage", [])) if total_storage < hw_req.min_storage_mb: warnings.append( f"Insufficient storage: {total_storage}MB available, " f"{hw_req.min_storage_mb}MB required" ) - + # Check GPU requirements if hw_req.requires_gpu: gpus = hw_profile.get("gpu", []) if not gpus: warnings.append("GPU required but not detected") elif hw_req.gpu_vendor: - vendor_match = any( - g.get("vendor") == hw_req.gpu_vendor for g in gpus - ) + vendor_match = any(g.get("vendor") == hw_req.gpu_vendor for g in gpus) if not vendor_match: - warnings.append( - f"{hw_req.gpu_vendor} GPU required but not found" - ) - + warnings.append(f"{hw_req.gpu_vendor} GPU required but not found") + # Check CUDA requirements if hw_req.requires_cuda: gpus = hw_profile.get("gpu", []) @@ -474,8 +478,8 @@ def check_hardware_compatibility(self, template: Template) -> Tuple[bool, List[s if hw_req.min_cuda_version: # Simple version comparison try: - gpu_cuda = tuple(map(int, cuda_version.split('.'))) - req_cuda = tuple(map(int, hw_req.min_cuda_version.split('.'))) + gpu_cuda = tuple(map(int, cuda_version.split("."))) + req_cuda = tuple(map(int, hw_req.min_cuda_version.split("."))) if gpu_cuda >= req_cuda: cuda_found = True break @@ -486,24 +490,22 @@ def check_hardware_compatibility(self, template: Template) -> Tuple[bool, List[s else: cuda_found = True break - + if not cuda_found: - warnings.append( - f"CUDA {hw_req.min_cuda_version or ''} required but not found" - ) - + warnings.append(f"CUDA {hw_req.min_cuda_version or ''} required but not found") + is_compatible = len(warnings) == 0 return is_compatible, warnings - + def generate_commands(self, template: Template) -> List[str]: """ Generate installation commands from template. - + Returns: List of installation commands """ commands = [] - + # If template has explicit steps, use those if template.steps: commands = [step.command for step in template.steps] @@ -518,71 +520,71 @@ def generate_commands(self, template: Template) -> List[str]: # Fallback: direct apt/yum install pm_type = pm.pm_type.value commands = [f"{pm_type} install -y {' '.join(template.packages)}"] - + return commands - + def import_template(self, file_path: str, name: Optional[str] = None) -> Template: """ Import a template from a file. - + Args: file_path: Path to template file name: Optional new name for the template - + Returns: Loaded template """ template_path = Path(file_path) if not template_path.exists(): raise FileNotFoundError(f"Template file not found: {file_path}") - + try: - with open(template_path, 'r', encoding='utf-8') as f: - if template_path.suffix in ['.yaml', '.yml']: + with open(template_path, "r", encoding="utf-8") as f: + if template_path.suffix in [".yaml", ".yml"]: data = yaml.safe_load(f) else: data = json.load(f) - + template = Template.from_dict(data) - + # Override name if provided if name: template.name = name - + # Validate is_valid, errors = TemplateValidator.validate(template) if not is_valid: raise ValueError(f"Template validation failed: {', '.join(errors)}") - + return template except Exception as e: raise ValueError(f"Failed to import template: {str(e)}") - - def export_template(self, name: str, file_path: str, - format: TemplateFormat = TemplateFormat.YAML) -> Path: + + def export_template( + self, name: str, file_path: str, format: TemplateFormat = TemplateFormat.YAML + ) -> Path: """ Export a template to a file. - + Args: name: Template name file_path: Destination file path format: File format - + Returns: Path to exported file """ template = self.load_template(name) if not template: raise ValueError(f"Template not found: {name}") - + export_path = Path(file_path) data = template.to_dict() - - with open(export_path, 'w', encoding='utf-8') as f: + + with open(export_path, "w", encoding="utf-8") as f: if format == TemplateFormat.YAML: yaml.dump(data, f, default_flow_style=False, sort_keys=False) else: json.dump(data, f, indent=2) - - return export_path + return export_path diff --git a/tests/test_templates.py b/tests/test_templates.py index 553449a3..ab829bf4 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -12,7 +12,8 @@ from pathlib import Path import sys -sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from cortex.templates import ( Template, @@ -20,13 +21,13 @@ TemplateValidator, TemplateFormat, HardwareRequirements, - InstallationStep + InstallationStep, ) class TestTemplate(unittest.TestCase): """Test Template dataclass.""" - + def test_template_creation(self): """Test creating a template.""" template = Template( @@ -34,31 +35,28 @@ def test_template_creation(self): description="Test template", version="1.0.0", author="Test Author", - packages=["package1", "package2"] + packages=["package1", "package2"], ) - + self.assertEqual(template.name, "test-template") self.assertEqual(template.description, "Test template") self.assertEqual(template.version, "1.0.0") self.assertEqual(template.author, "Test Author") self.assertEqual(len(template.packages), 2) - + def test_template_to_dict(self): """Test converting template to dictionary.""" template = Template( - name="test", - description="Test", - version="1.0.0", - packages=["pkg1", "pkg2"] + name="test", description="Test", version="1.0.0", packages=["pkg1", "pkg2"] ) - + data = template.to_dict() - + self.assertEqual(data["name"], "test") self.assertEqual(data["description"], "Test") self.assertEqual(data["version"], "1.0.0") self.assertEqual(data["packages"], ["pkg1", "pkg2"]) - + def test_template_from_dict(self): """Test creating template from dictionary.""" data = { @@ -70,35 +68,32 @@ def test_template_from_dict(self): { "command": "apt install pkg1", "description": "Install package 1", - "requires_root": True + "requires_root": True, } - ] + ], } - + template = Template.from_dict(data) - + self.assertEqual(template.name, "test") self.assertEqual(template.description, "Test description") self.assertEqual(len(template.packages), 2) self.assertEqual(len(template.steps), 1) self.assertEqual(template.steps[0].command, "apt install pkg1") - + def test_template_with_hardware_requirements(self): """Test template with hardware requirements.""" hw_req = HardwareRequirements( - min_ram_mb=4096, - min_cores=4, - requires_gpu=True, - gpu_vendor="NVIDIA" + min_ram_mb=4096, min_cores=4, requires_gpu=True, gpu_vendor="NVIDIA" ) - + template = Template( name="gpu-template", description="GPU template", version="1.0.0", - hardware_requirements=hw_req + hardware_requirements=hw_req, ) - + self.assertIsNotNone(template.hardware_requirements) self.assertEqual(template.hardware_requirements.min_ram_mb, 4096) self.assertEqual(template.hardware_requirements.requires_gpu, True) @@ -106,47 +101,36 @@ def test_template_with_hardware_requirements(self): class TestTemplateValidator(unittest.TestCase): """Test TemplateValidator.""" - + def test_validate_valid_template(self): """Test validating a valid template.""" template = Template( - name="valid-template", - description="Valid template", - version="1.0.0", - packages=["pkg1"] + name="valid-template", description="Valid template", version="1.0.0", packages=["pkg1"] ) - + is_valid, errors = TemplateValidator.validate(template) - + self.assertTrue(is_valid) self.assertEqual(len(errors), 0) - + def test_validate_missing_name(self): """Test validating template with missing name.""" - template = Template( - name="", - description="Test", - version="1.0.0" - ) - + template = Template(name="", description="Test", version="1.0.0") + is_valid, errors = TemplateValidator.validate(template) - + self.assertFalse(is_valid) self.assertIn("name is required", errors[0]) - + def test_validate_missing_packages_and_steps(self): """Test validating template with no packages or steps.""" - template = Template( - name="empty-template", - description="Empty template", - version="1.0.0" - ) - + template = Template(name="empty-template", description="Empty template", version="1.0.0") + is_valid, errors = TemplateValidator.validate(template) - + self.assertFalse(is_valid) self.assertIn("packages or steps", errors[0]) - + def test_validate_invalid_hardware_requirements(self): """Test validating template with invalid hardware requirements.""" hw_req = HardwareRequirements(min_ram_mb=-1) @@ -155,24 +139,24 @@ def test_validate_invalid_hardware_requirements(self): description="Invalid hardware", version="1.0.0", packages=["pkg1"], - hardware_requirements=hw_req + hardware_requirements=hw_req, ) - + is_valid, errors = TemplateValidator.validate(template) - + self.assertFalse(is_valid) self.assertIn("min_ram_mb", errors[0]) class TestTemplateManager(unittest.TestCase): """Test TemplateManager.""" - + def setUp(self): """Set up test fixtures.""" self.temp_dir = tempfile.mkdtemp() self.template_dir = Path(self.temp_dir) / "templates" self.template_dir.mkdir() - + # Create a test template self.test_template_data = { "name": "test-template", @@ -180,171 +164,148 @@ def setUp(self): "version": "1.0.0", "packages": ["package1", "package2"], "steps": [ - { - "command": "apt update", - "description": "Update packages", - "requires_root": True - } - ] + {"command": "apt update", "description": "Update packages", "requires_root": True} + ], } - + # Write test template to file template_file = self.template_dir / "test-template.yaml" - with open(template_file, 'w') as f: + with open(template_file, "w") as f: yaml.dump(self.test_template_data, f) - + def tearDown(self): """Clean up test fixtures.""" shutil.rmtree(self.temp_dir) - + def test_load_template(self): """Test loading a template.""" manager = TemplateManager(templates_dir=str(self.template_dir)) template = manager.load_template("test-template") - + self.assertIsNotNone(template) self.assertEqual(template.name, "test-template") self.assertEqual(len(template.packages), 2) - + def test_load_nonexistent_template(self): """Test loading a non-existent template.""" manager = TemplateManager(templates_dir=str(self.template_dir)) template = manager.load_template("nonexistent") - + self.assertIsNone(template) - + def test_save_template(self): """Test saving a template.""" manager = TemplateManager(templates_dir=str(self.template_dir)) - + template = Template( - name="new-template", - description="New template", - version="1.0.0", - packages=["pkg1"] + name="new-template", description="New template", version="1.0.0", packages=["pkg1"] ) - + template_path = manager.save_template(template, "new-template") - + self.assertTrue(template_path.exists()) - + # Verify it can be loaded loaded = manager.load_template("new-template") self.assertIsNotNone(loaded) self.assertEqual(loaded.name, "new-template") - + def test_list_templates(self): """Test listing templates.""" manager = TemplateManager(templates_dir=str(self.template_dir)) templates = manager.list_templates() - + self.assertIn("test-template", templates) self.assertEqual(templates["test-template"]["name"], "test-template") - + def test_generate_commands_from_packages(self): """Test generating commands from packages.""" manager = TemplateManager(templates_dir=str(self.template_dir)) - + template = Template( - name="test", - description="Test", - version="1.0.0", - packages=["package1", "package2"] + name="test", description="Test", version="1.0.0", packages=["package1", "package2"] ) - + commands = manager.generate_commands(template) - + self.assertGreater(len(commands), 0) self.assertTrue(any("package1" in cmd or "package2" in cmd for cmd in commands)) - + def test_generate_commands_from_steps(self): """Test generating commands from steps.""" manager = TemplateManager(templates_dir=str(self.template_dir)) - + template = Template( name="test", description="Test", version="1.0.0", - steps=[ - InstallationStep( - command="apt install pkg1", - description="Install pkg1" - ) - ] + steps=[InstallationStep(command="apt install pkg1", description="Install pkg1")], ) - + commands = manager.generate_commands(template) - + self.assertEqual(len(commands), 1) self.assertEqual(commands[0], "apt install pkg1") - + def test_import_template(self): """Test importing a template from file.""" manager = TemplateManager(templates_dir=str(self.template_dir)) - + # Create a temporary template file temp_file = Path(self.temp_dir) / "import-template.yaml" - with open(temp_file, 'w') as f: + with open(temp_file, "w") as f: yaml.dump(self.test_template_data, f) - + template = manager.import_template(str(temp_file)) - + self.assertIsNotNone(template) self.assertEqual(template.name, "test-template") - + def test_export_template(self): """Test exporting a template to file.""" manager = TemplateManager(templates_dir=str(self.template_dir)) - + export_path = Path(self.temp_dir) / "exported-template.yaml" manager.export_template("test-template", str(export_path)) - + self.assertTrue(export_path.exists()) - + # Verify content - with open(export_path, 'r') as f: + with open(export_path, "r") as f: data = yaml.safe_load(f) self.assertEqual(data["name"], "test-template") class TestHardwareCompatibility(unittest.TestCase): """Test hardware compatibility checking.""" - + def setUp(self): """Set up test fixtures.""" self.manager = TemplateManager() - + def test_check_hardware_compatibility_no_requirements(self): """Test checking compatibility with no requirements.""" - template = Template( - name="test", - description="Test", - version="1.0.0", - packages=["pkg1"] - ) - + template = Template(name="test", description="Test", version="1.0.0", packages=["pkg1"]) + is_compatible, warnings = self.manager.check_hardware_compatibility(template) - + self.assertTrue(is_compatible) self.assertEqual(len(warnings), 0) - + def test_check_hardware_compatibility_with_requirements(self): """Test checking compatibility with requirements.""" - hw_req = HardwareRequirements( - min_ram_mb=1024, - min_cores=2 - ) - + hw_req = HardwareRequirements(min_ram_mb=1024, min_cores=2) + template = Template( name="test", description="Test", version="1.0.0", packages=["pkg1"], - hardware_requirements=hw_req + hardware_requirements=hw_req, ) - + is_compatible, warnings = self.manager.check_hardware_compatibility(template) - + # Result depends on actual hardware, but should not crash self.assertIsInstance(is_compatible, bool) self.assertIsInstance(warnings, list) @@ -352,100 +313,93 @@ def test_check_hardware_compatibility_with_requirements(self): class TestTemplateFormat(unittest.TestCase): """Test template format handling.""" - + def setUp(self): """Set up test fixtures.""" self.temp_dir = tempfile.mkdtemp() self.template_dir = Path(self.temp_dir) / "templates" self.template_dir.mkdir() - + def tearDown(self): """Clean up test fixtures.""" shutil.rmtree(self.temp_dir) - + def test_save_yaml_format(self): """Test saving template in YAML format.""" manager = TemplateManager(templates_dir=str(self.template_dir)) - + template = Template( - name="yaml-test", - description="YAML test", - version="1.0.0", - packages=["pkg1"] + name="yaml-test", description="YAML test", version="1.0.0", packages=["pkg1"] ) - + template_path = manager.save_template(template, "yaml-test", TemplateFormat.YAML) - + self.assertTrue(template_path.exists()) self.assertEqual(template_path.suffix, ".yaml") - + # Verify it's valid YAML - with open(template_path, 'r') as f: + with open(template_path, "r") as f: data = yaml.safe_load(f) self.assertEqual(data["name"], "yaml-test") - + def test_save_json_format(self): """Test saving template in JSON format.""" manager = TemplateManager(templates_dir=str(self.template_dir)) - + template = Template( - name="json-test", - description="JSON test", - version="1.0.0", - packages=["pkg1"] + name="json-test", description="JSON test", version="1.0.0", packages=["pkg1"] ) - + template_path = manager.save_template(template, "json-test", TemplateFormat.JSON) - + self.assertTrue(template_path.exists()) self.assertEqual(template_path.suffix, ".json") - + # Verify it's valid JSON - with open(template_path, 'r') as f: + with open(template_path, "r") as f: data = json.load(f) self.assertEqual(data["name"], "json-test") - + def test_load_yaml_template(self): """Test loading YAML template.""" manager = TemplateManager(templates_dir=str(self.template_dir)) - + template_data = { "name": "yaml-load", "description": "YAML load test", "version": "1.0.0", - "packages": ["pkg1"] + "packages": ["pkg1"], } - + template_file = self.template_dir / "yaml-load.yaml" - with open(template_file, 'w') as f: + with open(template_file, "w") as f: yaml.dump(template_data, f) - + template = manager.load_template("yaml-load") - + self.assertIsNotNone(template) self.assertEqual(template.name, "yaml-load") - + def test_load_json_template(self): """Test loading JSON template.""" manager = TemplateManager(templates_dir=str(self.template_dir)) - + template_data = { "name": "json-load", "description": "JSON load test", "version": "1.0.0", - "packages": ["pkg1"] + "packages": ["pkg1"], } - + template_file = self.template_dir / "json-load.json" - with open(template_file, 'w') as f: + with open(template_file, "w") as f: json.dump(template_data, f) - + template = manager.load_template("json-load") - + self.assertIsNotNone(template) self.assertEqual(template.name, "json-load") -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() - From 176c17a101d24185e7beac54b27cde1a38236032 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Sat, 20 Dec 2025 13:24:05 +0500 Subject: [PATCH 07/16] again refomattrf files --- cortex/cli.py | 10 +++--- cortex/templates.py | 69 +++++++++++++++++++++-------------------- tests/test_templates.py | 24 +++++++------- 3 files changed, 52 insertions(+), 51 deletions(-) diff --git a/cortex/cli.py b/cortex/cli.py index b1be2671..e6152e2a 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -15,11 +15,11 @@ from cortex.branding import VERSION, console, cx_header, cx_print, show_banner from cortex.coordinator import InstallationCoordinator, StepStatus -from cortex.templates import TemplateManager, Template, TemplateFormat, InstallationStep from cortex.installation_history import InstallationHistory, InstallationStatus, InstallationType from cortex.llm.interpreter import CommandInterpreter from cortex.notification_manager import NotificationManager from cortex.stack_manager import StackManager +from cortex.templates import InstallationStep, Template, TemplateFormat, TemplateManager from cortex.user_preferences import ( PreferencesManager, format_preference_value, @@ -291,7 +291,7 @@ def install( software: str, execute: bool = False, dry_run: bool = False, - template: Optional[str] = None, + template: str | None = None, ): # Validate input first (only if not using template) if not template: @@ -554,7 +554,7 @@ def _install_from_template(self, template_name: str, execute: bool, dry_run: boo # Display template info print(f"\n{template.name} Template:") print(f" {template.description}") - print(f"\n Packages:") + print("\n Packages:") for pkg in template.packages: print(f" - {pkg}") @@ -768,7 +768,7 @@ def template_create(self, name: str, interactive: bool = True): packages.append(pkg) # Create template - from cortex.templates import Template, HardwareRequirements + from cortex.templates import HardwareRequirements, Template template = Template( name=name, @@ -811,7 +811,7 @@ def template_create(self, name: str, interactive: bool = True): self._print_error(f"Failed to create template: {str(e)}") return 1 - def template_import(self, file_path: str, name: Optional[str] = None): + def template_import(self, file_path: str, name: str | None = None): """Import a template from a file.""" try: template_manager = TemplateManager() diff --git a/cortex/templates.py b/cortex/templates.py index 2881b850..6cb32875 100644 --- a/cortex/templates.py +++ b/cortex/templates.py @@ -7,19 +7,20 @@ """ import json -import yaml import os import sys -from pathlib import Path -from typing import Dict, List, Optional, Any, Set, Tuple from dataclasses import dataclass, field from enum import Enum +from pathlib import Path +from typing import Any + +import yaml # Add parent directory to path for imports sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from src.hwprofiler import HardwareProfiler from cortex.packages import PackageManager, PackageManagerType +from src.hwprofiler import HardwareProfiler class TemplateFormat(Enum): @@ -33,13 +34,13 @@ class TemplateFormat(Enum): class HardwareRequirements: """Hardware requirements for a template.""" - min_ram_mb: Optional[int] = None - min_cores: Optional[int] = None - min_storage_mb: Optional[int] = None + min_ram_mb: int | None = None + min_cores: int | None = None + min_storage_mb: int | None = None requires_gpu: bool = False - gpu_vendor: Optional[str] = None # "NVIDIA", "AMD", "Intel" + gpu_vendor: str | None = None # "NVIDIA", "AMD", "Intel" requires_cuda: bool = False - min_cuda_version: Optional[str] = None + min_cuda_version: str | None = None @dataclass @@ -48,8 +49,8 @@ class InstallationStep: command: str description: str - rollback: Optional[str] = None - verify: Optional[str] = None + rollback: str | None = None + verify: str | None = None requires_root: bool = True @@ -60,15 +61,15 @@ class Template: name: str description: str version: str - author: Optional[str] = None - packages: List[str] = field(default_factory=list) - steps: List[InstallationStep] = field(default_factory=list) - hardware_requirements: Optional[HardwareRequirements] = None - post_install: List[str] = field(default_factory=list) - verification_commands: List[str] = field(default_factory=list) - metadata: Dict[str, Any] = field(default_factory=dict) - - def to_dict(self) -> Dict[str, Any]: + author: str | None = None + packages: list[str] = field(default_factory=list) + steps: list[InstallationStep] = field(default_factory=list) + hardware_requirements: HardwareRequirements | None = None + post_install: list[str] = field(default_factory=list) + verification_commands: list[str] = field(default_factory=list) + metadata: dict[str, Any] = field(default_factory=dict) + + def to_dict(self) -> dict[str, Any]: """Convert template to dictionary.""" result = { "name": self.name, @@ -110,7 +111,7 @@ def to_dict(self) -> Dict[str, Any]: return result @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "Template": + def from_dict(cls, data: dict[str, Any]) -> "Template": """Create template from dictionary.""" # Parse hardware requirements hw_req = None @@ -169,7 +170,7 @@ class TemplateValidator: DANGEROUS_SHELL_CHARS = [";", "|", "&", ">", "<", "`", "\\"] @staticmethod - def _validate_post_install_commands(post_install: List[str]) -> List[str]: + def _validate_post_install_commands(post_install: list[str]) -> list[str]: """ Validate post_install commands for security. @@ -216,7 +217,7 @@ def _validate_post_install_commands(post_install: List[str]) -> List[str]: return errors @staticmethod - def validate(template: Template) -> Tuple[bool, List[str]]: + def validate(template: Template) -> tuple[bool, list[str]]: """ Validate a template. @@ -269,7 +270,7 @@ def validate(template: Template) -> Tuple[bool, List[str]]: class TemplateManager: """Manages installation templates.""" - def __init__(self, templates_dir: Optional[str] = None): + def __init__(self, templates_dir: str | None = None): """ Initialize template manager. @@ -286,11 +287,11 @@ def __init__(self, templates_dir: Optional[str] = None): self.user_templates_dir = Path.home() / ".cortex" / "templates" self.user_templates_dir.mkdir(parents=True, exist_ok=True) - self._templates_cache: Dict[str, Template] = {} + self._templates_cache: dict[str, Template] = {} self._hardware_profiler = HardwareProfiler() self._package_manager = PackageManager() - def _get_template_path(self, name: str) -> Optional[Path]: + def _get_template_path(self, name: str) -> Path | None: """Find template file by name.""" # Check user templates first for ext in [".yaml", ".yml", ".json"]: @@ -306,7 +307,7 @@ def _get_template_path(self, name: str) -> Optional[Path]: return None - def load_template(self, name: str) -> Optional[Template]: + def load_template(self, name: str) -> Template | None: """Load a template by name.""" if name in self._templates_cache: return self._templates_cache[name] @@ -316,7 +317,7 @@ def load_template(self, name: str) -> Optional[Template]: return None try: - with open(template_path, "r", encoding="utf-8") as f: + with open(template_path, encoding="utf-8") as f: if template_path.suffix in [".yaml", ".yml"]: data = yaml.safe_load(f) else: @@ -331,7 +332,7 @@ def load_template(self, name: str) -> Optional[Template]: def save_template( self, template: Template, - name: Optional[str] = None, + name: str | None = None, format: TemplateFormat = TemplateFormat.YAML, ) -> Path: """ @@ -364,7 +365,7 @@ def save_template( return template_path - def list_templates(self) -> Dict[str, Dict[str, Any]]: + def list_templates(self) -> dict[str, dict[str, Any]]: """List all available templates.""" templates = {} @@ -417,7 +418,7 @@ def list_templates(self) -> Dict[str, Dict[str, Any]]: return templates - def check_hardware_compatibility(self, template: Template) -> Tuple[bool, List[str]]: + def check_hardware_compatibility(self, template: Template) -> tuple[bool, list[str]]: """ Check if current hardware meets template requirements. @@ -497,7 +498,7 @@ def check_hardware_compatibility(self, template: Template) -> Tuple[bool, List[s is_compatible = len(warnings) == 0 return is_compatible, warnings - def generate_commands(self, template: Template) -> List[str]: + def generate_commands(self, template: Template) -> list[str]: """ Generate installation commands from template. @@ -523,7 +524,7 @@ def generate_commands(self, template: Template) -> List[str]: return commands - def import_template(self, file_path: str, name: Optional[str] = None) -> Template: + def import_template(self, file_path: str, name: str | None = None) -> Template: """ Import a template from a file. @@ -539,7 +540,7 @@ def import_template(self, file_path: str, name: Optional[str] = None) -> Templat raise FileNotFoundError(f"Template file not found: {file_path}") try: - with open(template_path, "r", encoding="utf-8") as f: + with open(template_path, encoding="utf-8") as f: if template_path.suffix in [".yaml", ".yml"]: data = yaml.safe_load(f) else: diff --git a/tests/test_templates.py b/tests/test_templates.py index ab829bf4..0cad4769 100644 --- a/tests/test_templates.py +++ b/tests/test_templates.py @@ -3,25 +3,25 @@ Unit tests for Cortex Linux Template System """ -import unittest -import tempfile -import shutil -import os import json -import yaml +import os +import shutil +import sys +import tempfile +import unittest from pathlib import Path -import sys +import yaml sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from cortex.templates import ( + HardwareRequirements, + InstallationStep, Template, + TemplateFormat, TemplateManager, TemplateValidator, - TemplateFormat, - HardwareRequirements, - InstallationStep, ) @@ -271,7 +271,7 @@ def test_export_template(self): self.assertTrue(export_path.exists()) # Verify content - with open(export_path, "r") as f: + with open(export_path) as f: data = yaml.safe_load(f) self.assertEqual(data["name"], "test-template") @@ -338,7 +338,7 @@ def test_save_yaml_format(self): self.assertEqual(template_path.suffix, ".yaml") # Verify it's valid YAML - with open(template_path, "r") as f: + with open(template_path) as f: data = yaml.safe_load(f) self.assertEqual(data["name"], "yaml-test") @@ -356,7 +356,7 @@ def test_save_json_format(self): self.assertEqual(template_path.suffix, ".json") # Verify it's valid JSON - with open(template_path, "r") as f: + with open(template_path) as f: data = json.load(f) self.assertEqual(data["name"], "json-test") From b0bc267376ab28863f15f20bc8964fc95459d3fa Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Sat, 20 Dec 2025 13:26:22 +0500 Subject: [PATCH 08/16] refomattrf files --- cortex/templates.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cortex/templates.py b/cortex/templates.py index 6cb32875..2423df28 100644 --- a/cortex/templates.py +++ b/cortex/templates.py @@ -19,9 +19,10 @@ # Add parent directory to path for imports sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from cortex.packages import PackageManager, PackageManagerType from src.hwprofiler import HardwareProfiler +from cortex.packages import PackageManager, PackageManagerType + class TemplateFormat(Enum): """Supported template formats.""" @@ -378,7 +379,7 @@ def list_templates(self) -> dict[str, dict[str, Any]]: # Skip duplicate names across extensions continue try: - with open(template_file, "r", encoding="utf-8") as f: + with open(template_file, encoding="utf-8") as f: if template_file.suffix in [".yaml", ".yml"]: data = yaml.safe_load(f) else: From 50453dfd48b4f492443253aebe094ce4fc7490cb Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Sat, 20 Dec 2025 13:31:09 +0500 Subject: [PATCH 09/16] again refomattrf files --- src/intent/clarifier.py | 4 +--- src/intent/llm_agent.py | 20 +++++--------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/intent/clarifier.py b/src/intent/clarifier.py index ce8378c8..d49fd9a6 100644 --- a/src/intent/clarifier.py +++ b/src/intent/clarifier.py @@ -23,9 +23,7 @@ def needs_clarification(self, intents: list[Intent], text: str) -> str | None: # 2. If user says "machine learning tools" but nothing specific generic_terms = ["ml", "machine learning", "deep learning", "ai tools"] if any(term in text for term in generic_terms) and len(intents) == 0: - return ( - "Which ML frameworks do you need? (PyTorch, TensorFlow, JupyterLab...)" - ) + return "Which ML frameworks do you need? (PyTorch, TensorFlow, JupyterLab...)" # 3. If user asks to install CUDA but no GPU exists in context if any(i.target == "cuda" for i in intents) and "gpu" not in text: diff --git a/src/intent/llm_agent.py b/src/intent/llm_agent.py index f2d604ec..195a42f0 100644 --- a/src/intent/llm_agent.py +++ b/src/intent/llm_agent.py @@ -24,9 +24,7 @@ class LLMIntentAgent: - session context """ - def __init__( - self, api_key: str | None = None, model: str = "claude-3-5-sonnet-20240620" - ): + def __init__(self, api_key: str | None = None, model: str = "claude-3-5-sonnet-20240620"): # LLM is enabled ONLY if SDK + API key is available if Anthropic is None or api_key is None: @@ -92,9 +90,7 @@ def process(self, text: str): # ---------------------------------------------- # LLM enhancement of intents # ---------------------------------------------- - def enhance_intents_with_llm( - self, text: str, intents: list[Intent] - ) -> list[Intent]: + def enhance_intents_with_llm(self, text: str, intents: list[Intent]) -> list[Intent]: prompt = f""" You are an installation-intent expert. Convert the user request into structured intents. @@ -116,9 +112,7 @@ def enhance_intents_with_llm( ) # ---- Safety check ---- - if not getattr(response, "content", None) or not hasattr( - response.content[0], "text" - ): + if not getattr(response, "content", None) or not hasattr(response.content[0], "text"): return intents llm_output = response.content[0].text.lower().split("\n") @@ -161,13 +155,9 @@ def suggest_optimizations(self, text: str) -> list[str]: ) # ---- Safety check ---- - if not getattr(response, "content", None) or not hasattr( - response.content[0], "text" - ): + if not getattr(response, "content", None) or not hasattr(response.content[0], "text"): return [] return [ - line.strip() - for line in response.content[0].text.strip().split("\n") - if line.strip() + line.strip() for line in response.content[0].text.strip().split("\n") if line.strip() ] From eea8f48ab3853584a3677d929aa4db991660b05c Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Sat, 20 Dec 2025 13:40:57 +0500 Subject: [PATCH 10/16] fix unit tests --- cortex/templates.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cortex/templates.py b/cortex/templates.py index 2423df28..cafdeb23 100644 --- a/cortex/templates.py +++ b/cortex/templates.py @@ -19,8 +19,7 @@ # Add parent directory to path for imports sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from src.hwprofiler import HardwareProfiler - +from cortex.hwprofiler import HardwareProfiler from cortex.packages import PackageManager, PackageManagerType From a13d0925592a94ea0ab6b70c034853b858b83bad Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Fri, 26 Dec 2025 23:33:09 +0500 Subject: [PATCH 11/16] refactor: rename template system to stack and add comprehensive help docs --- cortex/cli.py | 425 ++++++++++++++++++++++++------- docs/{TEMPLATES.md => STACKS.md} | 192 ++++++++------ 2 files changed, 459 insertions(+), 158 deletions(-) rename docs/{TEMPLATES.md => STACKS.md} (71%) diff --git a/cortex/cli.py b/cortex/cli.py index 49117ff7..908162a8 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -215,7 +215,7 @@ def stack(self, args: argparse.Namespace) -> int: return 1 def _handle_stack_list(self, manager: StackManager) -> int: - """List all available stacks.""" + """List all available stacks (legacy simple stacks).""" stacks = manager.list_stacks() cx_print("\nšŸ“¦ Available Stacks:\n", "info") for stack in stacks: @@ -224,17 +224,18 @@ def _handle_stack_list(self, manager: StackManager) -> int: console.print(f" {stack.get('name', 'Unnamed Stack')}") console.print(f" {stack.get('description', 'No description')}") console.print(f" [dim]({pkg_count} packages)[/dim]\n") - cx_print("Use: cortex stack to install a stack", "info") + cx_print("Use: cortex install --stack --execute to install", "info") return 0 def _handle_stack_describe(self, manager: StackManager, stack_id: str) -> int: - """Describe a specific stack.""" + """Describe a specific stack (legacy simple stacks).""" stack = manager.find_stack(stack_id) if not stack: - self._print_error(f"Stack '{stack_id}' not found. Use --list to see available stacks.") + self._print_error(f"Stack '{stack_id}' not found. Use 'cortex stack list' to see available stacks.") return 1 description = manager.describe_stack(stack_id) console.print(description) + cx_print(f"\nTo install: cortex install --stack {stack_id} --execute", "info") return 0 def _handle_stack_install(self, manager: StackManager, args: argparse.Namespace) -> int: @@ -335,11 +336,11 @@ def install( software: str, execute: bool = False, dry_run: bool = False, - template: str | None = None, + stack: str | None = None, parallel: bool = False, ): - # Validate input first (only if not using template) - if not template: + # Validate input first (only if not using stack) + if not stack: is_valid, error = validate_install_request(software) if not is_valid: self._print_error(error) @@ -372,9 +373,9 @@ def install( start_time = datetime.now() try: - # If template is specified, use template system - if template: - return self._install_from_template(template, execute, dry_run) + # If stack is specified, use stack/template system + if stack: + return self._install_from_stack(stack, execute, dry_run) # Otherwise, use LLM-based installation self._print_status("🧠", "Understanding request...") @@ -694,8 +695,8 @@ def rollback(self, install_id: str, dry_run: bool = False): traceback.print_exc() return 1 - def _install_from_template(self, template_name: str, execute: bool, dry_run: bool): - """Install from a template.""" + def _install_from_stack(self, stack_name: str, execute: bool, dry_run: bool): + """Install from a stack (template).""" history = InstallationHistory() install_id = None start_time = datetime.now() @@ -703,19 +704,19 @@ def _install_from_template(self, template_name: str, execute: bool, dry_run: boo try: template_manager = TemplateManager() - self._print_status("[*]", f"Loading template: {template_name}...") - template = template_manager.load_template(template_name) + self._print_status("[*]", f"Loading stack: {stack_name}...") + template = template_manager.load_template(stack_name) if not template: - self._print_error(f"Template '{template_name}' not found") - self._print_status("[*]", "Available templates:") + self._print_error(f"Stack '{stack_name}' not found") + self._print_status("[*]", "Available stacks:") templates = template_manager.list_templates() for name, info in templates.items(): print(f" - {name}: {info['description']}") return 1 - # Display template info - print(f"\n{template.name} Template:") + # Display stack info + print(f"\n{template.name} Stack:") print(f" {template.description}") print("\n Packages:") for pkg in template.packages: @@ -864,7 +865,7 @@ def progress_callback(current, total, step): return 1 else: print("\nTo execute these commands, run with --execute flag") - print(f"Example: cortex install --template {template_name} --execute") + print(f"Example: cortex install --stack {stack_name} --execute") return 0 @@ -879,17 +880,17 @@ def progress_callback(current, total, step): self._print_error(f"Unexpected error: {str(e)}") return 1 - def template_list(self): - """List all available templates.""" + def stack_list(self): + """List all available stacks.""" try: template_manager = TemplateManager() templates = template_manager.list_templates() if not templates: - print("No templates found.") + print("No stacks found.") return 0 - print("\nAvailable Templates:") + print("\nAvailable Stacks:") print("=" * 80) print(f"{'Name':<20} {'Version':<12} {'Type':<12} {'Description':<35}") print("=" * 80) @@ -902,16 +903,80 @@ def template_list(self): ) print(f"{name:<20} {info['version']:<12} {info['type']:<12} {desc:<35}") - print(f"\nTotal: {len(templates)} templates") + print(f"\nTotal: {len(templates)} stacks") + print("\nTo install a stack:") + print(" cortex install --stack --dry-run # Preview") + print(" cortex install --stack --execute # Install") + return 0 + except Exception as e: + self._print_error(f"Failed to list stacks: {str(e)}") + return 1 + + def stack_describe(self, name: str): + """Show detailed information about a stack.""" + try: + template_manager = TemplateManager() + template = template_manager.load_template(name) + + if not template: + self._print_error(f"Stack '{name}' not found") + print("\nAvailable stacks:") + templates = template_manager.list_templates() + for stack_name in sorted(templates.keys()): + print(f" - {stack_name}") + return 1 + + # Display stack details + print(f"\nšŸ“¦ Stack: {template.name}") + print(f" {template.description}") + print(f" Version: {template.version}") + if template.author: + print(f" Author: {template.author}") + + print("\n Packages:") + for pkg in template.packages: + print(f" - {pkg}") + + # Show hardware requirements if present + if template.hardware_requirements: + hw = template.hardware_requirements + print("\n Hardware Requirements:") + if hw.min_ram_mb: + print(f" - Minimum RAM: {hw.min_ram_mb}MB") + if hw.min_cores: + print(f" - Minimum CPU cores: {hw.min_cores}") + if hw.min_storage_mb: + print(f" - Minimum storage: {hw.min_storage_mb}MB") + if hw.requires_gpu: + gpu_info = f"Required" + if hw.gpu_vendor: + gpu_info += f" ({hw.gpu_vendor})" + print(f" - GPU: {gpu_info}") + if hw.requires_cuda: + cuda_info = "Required" + if hw.min_cuda_version: + cuda_info += f" (>= {hw.min_cuda_version})" + print(f" - CUDA: {cuda_info}") + + # Show verification commands if present + if template.verification_commands: + print("\n Verification commands:") + for cmd in template.verification_commands: + print(f" $ {cmd}") + + print("\n To install this stack:") + print(f" cortex install --stack {name} --dry-run # Preview") + print(f" cortex install --stack {name} --execute # Install") + return 0 except Exception as e: - self._print_error(f"Failed to list templates: {str(e)}") + self._print_error(f"Failed to describe stack: {str(e)}") return 1 - def template_create(self, name: str, interactive: bool = True): - """Create a new template interactively.""" + def stack_create(self, name: str, interactive: bool = True): + """Create a new stack interactively.""" try: - print(f"\n[*] Creating template: {name}") + print(f"\n[*] Creating stack: {name}") if interactive: description = input("Description: ").strip() @@ -930,7 +995,7 @@ def template_create(self, name: str, interactive: bool = True): break packages.append(pkg) - # Create template + # Create stack template from cortex.templates import HardwareRequirements, Template template = Template( @@ -959,40 +1024,40 @@ def template_create(self, name: str, interactive: bool = True): return 1 template.hardware_requirements = hw_req - # Save template + # Save stack template_manager = TemplateManager() template_path = template_manager.save_template(template, name) - self._print_success(f"Template '{name}' created successfully!") + self._print_success(f"Stack '{name}' created successfully!") print(f" Saved to: {template_path}") return 0 else: - self._print_error("Non-interactive template creation not yet supported") + self._print_error("Non-interactive stack creation not yet supported") return 1 except Exception as e: - self._print_error(f"Failed to create template: {str(e)}") + self._print_error(f"Failed to create stack: {str(e)}") return 1 - def template_import(self, file_path: str, name: str | None = None): - """Import a template from a file.""" + def stack_import(self, file_path: str, name: str | None = None): + """Import a stack from a file.""" try: template_manager = TemplateManager() template = template_manager.import_template(file_path, name) - # Save to user templates + # Save to user stacks save_name = name or template.name template_path = template_manager.save_template(template, save_name) - self._print_success(f"Template '{save_name}' imported successfully!") + self._print_success(f"Stack '{save_name}' imported successfully!") print(f" Saved to: {template_path}") return 0 except Exception as e: - self._print_error(f"Failed to import template: {str(e)}") + self._print_error(f"Failed to import stack: {str(e)}") return 1 - def template_export(self, name: str, file_path: str, format: str = "yaml"): - """Export a template to a file.""" + def stack_export(self, name: str, file_path: str, format: str = "yaml"): + """Export a stack to a file.""" try: template_manager = TemplateManager() template_format = ( @@ -1000,11 +1065,11 @@ def template_export(self, name: str, file_path: str, format: str = "yaml"): ) export_path = template_manager.export_template(name, file_path, template_format) - self._print_success(f"Template '{name}' exported successfully!") + self._print_success(f"Stack '{name}' exported successfully!") print(f" Saved to: {export_path}") return 0 except Exception as e: - self._print_error(f"Failed to export template: {str(e)}") + self._print_error(f"Failed to export stack: {str(e)}") return 1 def _get_prefs_manager(self): @@ -1517,21 +1582,27 @@ def show_rich_help(): table.add_column("Command", style="green") table.add_column("Description") + table.add_row("install ", "Install software using natural language") + table.add_row("install --stack ", "Install a pre-configured stack") + table.add_row("stack list", "List available stacks") + table.add_row("stack create ", "Create a custom stack") + table.add_row("stack export/import", "Share stacks with others") table.add_row("ask ", "Ask about your system") - table.add_row("demo", "See Cortex in action") - table.add_row("wizard", "Configure API key") - table.add_row("status", "System status") - table.add_row("install ", "Install software") - table.add_row("history", "View history") - table.add_row("rollback ", "Undo installation") - table.add_row("notify", "Manage desktop notifications") - table.add_row("env", "Manage environment variables") - table.add_row("cache stats", "Show LLM cache statistics") - table.add_row("stack ", "Install the stack") + table.add_row("history", "View installation history") + table.add_row("rollback ", "Undo an installation") table.add_row("doctor", "System health check") + table.add_row("status", "Show system status") + table.add_row("env", "Manage environment variables") + table.add_row("wizard", "Configure API key") console.print(table) console.print() + + console.print("[bold cyan]Quick Examples:[/bold cyan]") + console.print(" cortex install docker --execute") + console.print(" cortex install --stack lamp --dry-run") + console.print(" cortex stack list") + console.print() console.print("[dim]Learn more: https://cortexlinux.com/docs[/dim]") @@ -1587,27 +1658,33 @@ def main(): formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: + # Natural language installation cortex install docker cortex install docker --execute cortex install "python 3.11 with pip" cortex install nginx --dry-run - cortex install --template lamp --execute - cortex template list - cortex template create my-stack - cortex template import template.yaml - cortex template export lamp my-template.yaml + + # Stack-based installation + cortex install --stack lamp --dry-run # Preview LAMP stack + cortex install --stack lamp --execute # Install LAMP stack + cortex install --stack ml-ai --execute # Install ML/AI stack + + # Stack management + cortex stack list # List all stacks + cortex stack describe lamp # Show stack details + cortex stack create my-stack # Create custom stack + cortex stack export lamp my-lamp.yaml # Export for sharing + cortex stack import my-lamp.yaml # Import a stack + + # History and rollback cortex history cortex history show cortex rollback - cortex check-pref - cortex check-pref ai.model - cortex edit-pref set ai.model gpt-4 - cortex edit-pref delete theme - cortex edit-pref reset-all Environment Variables: OPENAI_API_KEY OpenAI API key for GPT-4 ANTHROPIC_API_KEY Anthropic API key for Claude + CORTEX_PROVIDER LLM provider (claude, openai, ollama) """, ) @@ -1638,14 +1715,40 @@ def main(): # Install command install_parser = subparsers.add_parser( - "install", help="Install software using natural language or template" + "install", + help="Install software using natural language or stack", + formatter_class=argparse.RawDescriptionHelpFormatter, + description="""Install software packages using natural language or pre-configured stacks. + +Cortex uses AI to understand your installation requests and generates +the appropriate commands for your system.""", + epilog=""" +Examples: + # Natural language installation + cortex install docker + cortex install "python 3.11 with pip" + cortex install nginx --dry-run + cortex install docker --execute + + # Stack-based installation (pre-configured bundles) + cortex install --stack lamp --dry-run # Preview LAMP stack + cortex install --stack lamp --execute # Install LAMP stack + cortex install --stack mern --execute # Install MERN stack + cortex install --stack ml-ai --execute # Install ML/AI stack + +Available stacks: lamp, mean, mern, ml-ai, devops +Use 'cortex stack list' to see all available stacks. +""", ) install_group = install_parser.add_mutually_exclusive_group(required=True) install_group.add_argument( "software", type=str, nargs="?", help="Software to install (natural language)" ) install_group.add_argument( - "--template", type=str, help="Install from template (e.g., lamp, mean, mern)" + "--stack", + type=str, + metavar="NAME", + help="Install from a pre-configured stack (e.g., lamp, mean, mern, ml-ai, devops)", ) install_parser.add_argument( "--execute", action="store_true", help="Execute the generated commands" @@ -1700,36 +1803,149 @@ def main(): send_parser.add_argument("--actions", nargs="*", help="Action buttons") # -------------------------- - # Template commands - template_parser = subparsers.add_parser("template", help="Manage installation templates") + # Template commands - DEPRECATED: Use 'cortex stack' instead + # Kept for backward compatibility, redirects to stack commands + template_parser = subparsers.add_parser( + "template", + help="[DEPRECATED] Use 'cortex stack' instead", + description="DEPRECATED: This command has been renamed to 'cortex stack'.\n" + "Please use 'cortex stack list', 'cortex stack create', etc.", + ) template_subs = template_parser.add_subparsers(dest="template_action", help="Template actions") - template_subs.add_parser("list", help="List all available templates") + template_subs.add_parser("list", help="[DEPRECATED] Use 'cortex stack list'") - template_create_parser = template_subs.add_parser("create", help="Create a new template") + template_create_parser = template_subs.add_parser( + "create", help="[DEPRECATED] Use 'cortex stack create'" + ) template_create_parser.add_argument("name", help="Template name") - template_import_parser = template_subs.add_parser("import", help="Import a template from file") + template_import_parser = template_subs.add_parser( + "import", help="[DEPRECATED] Use 'cortex stack import'" + ) template_import_parser.add_argument("file_path", help="Path to template file") template_import_parser.add_argument("--name", help="Override template name") - template_export_parser = template_subs.add_parser("export", help="Export a template to file") + template_export_parser = template_subs.add_parser( + "export", help="[DEPRECATED] Use 'cortex stack export'" + ) template_export_parser.add_argument("name", help="Template name") template_export_parser.add_argument("file_path", help="Output file path") template_export_parser.add_argument( "--format", choices=["yaml", "json"], default="yaml", help="Export format" ) - # Stack command - stack_parser = subparsers.add_parser("stack", help="Manage pre-built package stacks") - stack_parser.add_argument( - "name", nargs="?", help="Stack name to install (ml, ml-cpu, webdev, devops, data)" + # Stack command - enhanced with create/import/export subcommands + stack_parser = subparsers.add_parser( + "stack", + help="Manage installation stacks for common development environments", + formatter_class=argparse.RawDescriptionHelpFormatter, + description="""Manage installation stacks for common development environments. + +Stacks are pre-configured bundles of packages and tools that can be installed +together. Cortex provides built-in stacks for common use cases and supports +custom stack creation.""", + epilog=""" +Examples: + # List all available stacks + cortex stack list + + # Show details about a specific stack + cortex stack describe lamp + + # Install a stack (use install command) + cortex install --stack lamp --dry-run # Preview first + cortex install --stack lamp --execute # Install + + # Create a custom stack interactively + cortex stack create my-dev-stack + + # Export a stack to share with others + cortex stack export lamp my-lamp.yaml + + # Import a stack from a file + cortex stack import my-lamp.yaml + cortex stack import my-lamp.yaml --name custom-lamp + +Built-in stacks: + lamp - Linux, Apache, MySQL, PHP web server stack + mean - MongoDB, Express.js, Angular, Node.js + mern - MongoDB, Express.js, React, Node.js + ml-ai - Machine Learning with Python, TensorFlow, PyTorch + devops - Docker, Kubernetes, Terraform, Ansible + +Use 'cortex stack list' for a complete list with descriptions. +""", ) - stack_group = stack_parser.add_mutually_exclusive_group() - stack_group.add_argument("--list", "-l", action="store_true", help="List all available stacks") - stack_group.add_argument("--describe", "-d", metavar="STACK", help="Show details about a stack") - stack_parser.add_argument( - "--dry-run", action="store_true", help="Show what would be installed (requires stack name)" + stack_subs = stack_parser.add_subparsers(dest="stack_action", help="Stack actions") + + # stack list + stack_list_parser = stack_subs.add_parser( + "list", + help="List all available stacks", + description="Display all available stacks with their versions and descriptions.", + epilog=""" +Examples: + cortex stack list +""", ) + + # stack describe + stack_describe_parser = stack_subs.add_parser( + "describe", + help="Show detailed information about a stack", + description="Display detailed information about a specific stack including packages and requirements.", + epilog=""" +Examples: + cortex stack describe lamp + cortex stack describe ml-ai +""", + ) + stack_describe_parser.add_argument("name", help="Stack name to describe") + + # stack create + stack_create_parser = stack_subs.add_parser( + "create", + help="Create a new custom stack interactively", + description="Create a new custom stack with packages and hardware requirements.", + epilog=""" +Examples: + cortex stack create my-web-stack + cortex stack create ml-custom +""", + ) + stack_create_parser.add_argument("name", help="Name for the new stack") + + # stack import [--name NAME] + stack_import_parser = stack_subs.add_parser( + "import", + help="Import a stack from a YAML/JSON file", + description="Import a stack definition from a YAML or JSON file.", + epilog=""" +Examples: + cortex stack import my-stack.yaml + cortex stack import team-stack.json --name my-team-stack +""", + ) + stack_import_parser.add_argument("file_path", help="Path to stack file (YAML or JSON)") + stack_import_parser.add_argument("--name", help="Override the stack name") + + # stack export [--format FORMAT] + stack_export_parser = stack_subs.add_parser( + "export", + help="Export a stack to a file for sharing", + description="Export a stack definition to a YAML or JSON file.", + epilog=""" +Examples: + cortex stack export lamp my-lamp.yaml + cortex stack export ml-ai ml-stack.json --format json +""", + ) + stack_export_parser.add_argument("name", help="Stack name to export") + stack_export_parser.add_argument("file_path", help="Output file path") + stack_export_parser.add_argument( + "--format", choices=["yaml", "json"], default="yaml", help="Export format (default: yaml)" + ) + # Cache commands cache_parser = subparsers.add_parser("cache", help="Cache operations") cache_subs = cache_parser.add_subparsers(dest="cache_action", help="Cache actions") @@ -1846,9 +2062,9 @@ def main(): elif args.command == "ask": return cli.ask(args.question) elif args.command == "install": - if args.template: + if args.stack: return cli.install( - "", execute=args.execute, dry_run=args.dry_run, template=args.template + "", execute=args.execute, dry_run=args.dry_run, stack=args.stack ) else: # software is guaranteed to be set due to mutually_exclusive_group(required=True) @@ -1863,15 +2079,24 @@ def main(): elif args.command == "rollback": return cli.rollback(args.id, dry_run=args.dry_run) elif args.command == "template": + # DEPRECATED: Redirect to stack commands with warning + cx_print( + "āš ļø 'cortex template' is deprecated. Use 'cortex stack' instead.", "warning" + ) if args.template_action == "list": - return cli.template_list() + cx_print(" Use: cortex stack list", "info") + return cli.stack_list() elif args.template_action == "create": - return cli.template_create(args.name) + cx_print(f" Use: cortex stack create {args.name}", "info") + return cli.stack_create(args.name) elif args.template_action == "import": - return cli.template_import(args.file_path, args.name) + cx_print(f" Use: cortex stack import {args.file_path}", "info") + return cli.stack_import(args.file_path, args.name) elif args.template_action == "export": - return cli.template_export(args.name, args.file_path, args.format) + cx_print(f" Use: cortex stack export {args.name} {args.file_path}", "info") + return cli.stack_export(args.name, args.file_path, args.format) else: + cx_print(" Use: cortex stack --help", "info") parser.print_help() return 1 elif args.command == "check-pref": @@ -1882,7 +2107,35 @@ def main(): elif args.command == "notify": return cli.notify(args) elif args.command == "stack": - return cli.stack(args) + # Handle subcommands (list, describe, create, import, export) + stack_action = getattr(args, "stack_action", None) + if stack_action == "list": + return cli.stack_list() + elif stack_action == "describe": + return cli.stack_describe(args.name) + elif stack_action == "create": + return cli.stack_create(args.name) + elif stack_action == "import": + return cli.stack_import(args.file_path, getattr(args, "name", None)) + elif stack_action == "export": + return cli.stack_export(args.name, args.file_path, args.format) + else: + # No subcommand - show help and list stacks + cx_print("Use 'cortex stack ' to manage stacks.", "info") + cx_print("", "info") + cx_print("Commands:", "info") + cx_print(" list - List all available stacks", "info") + cx_print(" describe - Show details about a stack", "info") + cx_print(" create - Create a new custom stack", "info") + cx_print(" import - Import a stack from file", "info") + cx_print(" export - Export a stack to file", "info") + cx_print("", "info") + cx_print("To install a stack, use:", "info") + cx_print(" cortex install --stack --dry-run", "info") + cx_print(" cortex install --stack --execute", "info") + cx_print("", "info") + cx_print("Run 'cortex stack --help' for more information.", "info") + return 0 elif args.command == "doctor": return cli.doctor() elif args.command == "cache": diff --git a/docs/TEMPLATES.md b/docs/STACKS.md similarity index 71% rename from docs/TEMPLATES.md rename to docs/STACKS.md index 4fc12347..085587e0 100644 --- a/docs/TEMPLATES.md +++ b/docs/STACKS.md @@ -1,55 +1,57 @@ -# Installation Templates Guide +# Installation Stacks Guide -Cortex Linux provides a powerful template system for installing common development stacks and software bundles. Templates are pre-configured installation definitions that can be shared, customized, and reused. +Cortex Linux provides a powerful stack system for installing common development environments and software bundles. Stacks are pre-configured installation definitions that can be shared, customized, and reused. ## Overview -Templates allow you to: -- Install complete development stacks with a single command +Stacks allow you to: +- Install complete development environments with a single command - Share installation configurations with your team -- Create custom templates for your specific needs +- Create custom stacks for your specific needs - Validate hardware compatibility before installation -- Export and import templates for easy sharing +- Export and import stacks for easy sharing ## Quick Start -### Installing from a Template +### Installing from a Stack ```bash -# List available templates -cortex template list +# List available stacks +cortex stack list + +# Install LAMP stack (preview first) +cortex install --stack lamp --dry-run # Install LAMP stack -cortex install --template lamp --execute +cortex install --stack lamp --execute -# Install MEAN stack (dry run first) -cortex install --template mean --dry-run -cortex install --template mean --execute +# Install MEAN stack +cortex install --stack mean --execute ``` -### Creating a Custom Template +### Creating a Custom Stack ```bash -# Create a new template interactively -cortex template create my-stack +# Create a new stack interactively +cortex stack create my-stack -# Import a template from file -cortex template import my-template.yaml +# Import a stack from file +cortex stack import my-stack.yaml -# Export a template -cortex template export lamp my-lamp-template.yaml +# Export a stack +cortex stack export lamp my-lamp-stack.yaml ``` -## Built-in Templates +## Built-in Stacks -Cortex Linux comes with 5+ pre-built templates: +Cortex Linux comes with 5+ pre-built stacks: ### 1. LAMP Stack Linux, Apache, MySQL, PHP stack for traditional web development. ```bash -cortex install --template lamp --execute +cortex install --stack lamp --execute ``` **Packages:** @@ -72,7 +74,7 @@ cortex install --template lamp --execute MongoDB, Express.js, Angular, Node.js stack for modern web applications. ```bash -cortex install --template mean --execute +cortex install --stack mean --execute ``` **Packages:** @@ -91,7 +93,7 @@ cortex install --template mean --execute MongoDB, Express.js, React, Node.js stack for full-stack JavaScript development. ```bash -cortex install --template mern --execute +cortex install --stack mern --execute ``` **Packages:** @@ -110,7 +112,7 @@ cortex install --template mern --execute Machine Learning and Artificial Intelligence development stack. ```bash -cortex install --template ml-ai --execute +cortex install --stack ml-ai --execute ``` **Packages:** @@ -132,7 +134,7 @@ cortex install --template ml-ai --execute Complete DevOps toolchain with containerization and infrastructure tools. ```bash -cortex install --template devops --execute +cortex install --stack devops --execute ``` **Packages:** @@ -148,9 +150,9 @@ cortex install --template devops --execute - Minimum CPU cores: 4 - Minimum storage: 20GB -## Template Format +## Stack Format -Templates are defined in YAML or JSON format. Here's the structure: +Stacks are defined in YAML or JSON format. Here's the structure: ### YAML Format @@ -229,17 +231,17 @@ metadata: } ``` -## Template Fields +## Stack Fields ### Required Fields -- **name**: Template name (string) -- **description**: Template description (string) -- **version**: Template version (string, e.g., "1.0.0") +- **name**: Stack name (string) +- **description**: Stack description (string) +- **version**: Stack version (string, e.g., "1.0.0") ### Optional Fields -- **author**: Template author (string) +- **author**: Stack author (string) - **packages**: List of package names (array of strings) - **steps**: Installation steps (array of step objects) - **hardware_requirements**: Hardware requirements (object) @@ -266,12 +268,12 @@ Each step can have: - **requires_cuda**: Whether CUDA is required (boolean) - **min_cuda_version**: Minimum CUDA version (string, e.g., "11.0") -## Creating Custom Templates +## Creating Custom Stacks ### Interactive Creation ```bash -cortex template create my-stack +cortex stack create my-stack ``` This will prompt you for: @@ -295,29 +297,29 @@ packages: - docker ``` -2. Import the template: +2. Import the stack: ```bash -cortex template import my-template.yaml +cortex stack import my-stack.yaml ``` -3. Use the template: +3. Use the stack: ```bash -cortex install --template my-custom-stack --execute +cortex install --stack my-custom-stack --execute ``` -## Template Management +## Stack Management -### Listing Templates +### Listing Stacks ```bash -cortex template list +cortex stack list ``` Output: ``` -šŸ“‹ Available Templates: +šŸ“‹ Available Stacks: ================================================================================ Name Version Type Description ================================================================================ @@ -327,37 +329,69 @@ mean 1.0.0 built-in MongoDB, Express.js, Angular... mern 1.0.0 built-in MongoDB, Express.js, React... ml-ai 1.0.0 built-in Machine Learning and AI... -Total: 5 templates +Total: 5 stacks + +To install a stack: + cortex install --stack --dry-run # Preview + cortex install --stack --execute # Install +``` + +### Describing Stacks + +```bash +cortex stack describe lamp +``` + +Output: +``` +šŸ“¦ Stack: LAMP Stack + Linux, Apache, MySQL, PHP development stack + Version: 1.0.0 + + Packages: + - apache2 + - mysql-server + - php + - phpmyadmin + + Hardware Requirements: + - Minimum RAM: 1024MB + - Minimum CPU cores: 2 + - Minimum storage: 5120MB + + To install this stack: + cortex install --stack lamp --dry-run # Preview + cortex install --stack lamp --execute # Install ``` -### Exporting Templates +### Exporting Stacks ```bash # Export to YAML (default) -cortex template export lamp my-lamp-template.yaml +cortex stack export lamp my-lamp-stack.yaml # Export to JSON -cortex template export lamp my-lamp-template.json --format json +cortex stack export lamp my-lamp-stack.json --format json ``` -### Importing Templates +### Importing Stacks ```bash # Import with original name -cortex template import my-template.yaml +cortex stack import my-stack.yaml # Import with custom name -cortex template import my-template.yaml --name my-custom-name +cortex stack import my-stack.yaml --name my-custom-name ``` ## Hardware Compatibility -Templates can specify hardware requirements. Cortex will check compatibility before installation: +Stacks can specify hardware requirements. Cortex will check compatibility before installation: ```bash -$ cortex install --template ml-ai --execute +$ cortex install --stack ml-ai --execute -šŸ“‹ ML/AI Stack Template: +šŸ“¦ ML/AI Stack: Machine Learning and Artificial Intelligence development stack Packages: @@ -371,9 +405,9 @@ $ cortex install --template ml-ai --execute āš ļø Hardware requirements not met. Continue anyway? (y/N): ``` -## Template Validation +## Stack Validation -Templates are automatically validated before installation. Validation checks: +Stacks are automatically validated before installation. Validation checks: - Required fields are present - At least packages or steps are defined @@ -381,7 +415,7 @@ Templates are automatically validated before installation. Validation checks: - Hardware requirements are valid (non-negative values) - CUDA requirements are consistent with GPU requirements -## Example Templates +## Example Stacks ### Python Data Science Stack @@ -459,9 +493,9 @@ post_install: ## Best Practices -1. **Always test templates in dry-run mode first:** +1. **Always test stacks in dry-run mode first:** ```bash - cortex install --template my-template --dry-run + cortex install --stack my-stack --dry-run ``` 2. **Specify hardware requirements** to help users understand system needs @@ -472,22 +506,22 @@ post_install: 5. **Use descriptive step descriptions** for better user experience -6. **Version your templates** to track changes +6. **Version your stacks** to track changes 7. **Document post-installation steps** in post_install commands ## Troubleshooting -### Template Not Found +### Stack Not Found -If a template is not found, check: -- Template name is correct (use `cortex template list` to verify) -- Template file exists in `~/.cortex/templates/` or built-in templates directory +If a stack is not found, check: +- Stack name is correct (use `cortex stack list` to verify) +- Stack file exists in `~/.cortex/templates/` or built-in stacks directory - File extension is `.yaml`, `.yml`, or `.json` ### Validation Errors -If template validation fails: +If stack validation fails: - Check all required fields are present - Ensure at least packages or steps are defined - Verify hardware requirements are non-negative @@ -498,12 +532,12 @@ If template validation fails: If hardware compatibility warnings appear: - Review the warnings carefully - Consider if the installation will work with your hardware -- Some templates may work with less hardware but with reduced performance +- Some stacks may work with less hardware but with reduced performance - You can proceed anyway if you understand the risks -## Template Sharing +## Stack Sharing -Templates can be shared by: +Stacks can be shared by: 1. Exporting to a file 2. Sharing the file via version control, email, or file sharing 3. Importing on another system @@ -511,13 +545,13 @@ Templates can be shared by: Example workflow: ```bash # On source system -cortex template export my-stack my-stack.yaml +cortex stack export my-stack my-stack.yaml # Share my-stack.yaml # On target system -cortex template import my-stack.yaml -cortex install --template my-stack --execute +cortex stack import my-stack.yaml +cortex install --stack my-stack --execute ``` ## Advanced Usage @@ -540,7 +574,7 @@ steps: ### Conditional Installation -While templates don't support conditional logic directly, you can use shell commands: +While stacks don't support conditional logic directly, you can use shell commands: ```yaml steps: @@ -562,6 +596,20 @@ post_install: - echo "Service configured. Access at http://localhost:8080" ``` +## Migration from Templates + +If you were using the older `cortex template` commands, they have been renamed to `cortex stack`: + +| Old Command | New Command | +|------------|-------------| +| `cortex template list` | `cortex stack list` | +| `cortex template create ` | `cortex stack create ` | +| `cortex template import ` | `cortex stack import ` | +| `cortex template export ` | `cortex stack export ` | +| `cortex install --template ` | `cortex install --stack ` | + +The old `cortex template` commands still work but will show a deprecation warning. + ## See Also - [User Guide](../User-Guide.md) - General Cortex usage From 88379479748250c99ac7529c4c3ebfbb6aa8aca3 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Fri, 26 Dec 2025 23:36:51 +0500 Subject: [PATCH 12/16] fix lint error --- cortex/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cortex/cli.py b/cortex/cli.py index 908162a8..dbb36d1f 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -948,7 +948,7 @@ def stack_describe(self, name: str): if hw.min_storage_mb: print(f" - Minimum storage: {hw.min_storage_mb}MB") if hw.requires_gpu: - gpu_info = f"Required" + gpu_info = "Required" if hw.gpu_vendor: gpu_info += f" ({hw.gpu_vendor})" print(f" - GPU: {gpu_info}") From aa390a693e07b0dfc3c91a76b0158627d324f0c0 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Fri, 26 Dec 2025 23:38:08 +0500 Subject: [PATCH 13/16] reformatted file --- cortex/cli.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/cortex/cli.py b/cortex/cli.py index dbb36d1f..2d745706 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -231,7 +231,9 @@ def _handle_stack_describe(self, manager: StackManager, stack_id: str) -> int: """Describe a specific stack (legacy simple stacks).""" stack = manager.find_stack(stack_id) if not stack: - self._print_error(f"Stack '{stack_id}' not found. Use 'cortex stack list' to see available stacks.") + self._print_error( + f"Stack '{stack_id}' not found. Use 'cortex stack list' to see available stacks." + ) return 1 description = manager.describe_stack(stack_id) console.print(description) @@ -2063,9 +2065,7 @@ def main(): return cli.ask(args.question) elif args.command == "install": if args.stack: - return cli.install( - "", execute=args.execute, dry_run=args.dry_run, stack=args.stack - ) + return cli.install("", execute=args.execute, dry_run=args.dry_run, stack=args.stack) else: # software is guaranteed to be set due to mutually_exclusive_group(required=True) return cli.install( @@ -2080,9 +2080,7 @@ def main(): return cli.rollback(args.id, dry_run=args.dry_run) elif args.command == "template": # DEPRECATED: Redirect to stack commands with warning - cx_print( - "āš ļø 'cortex template' is deprecated. Use 'cortex stack' instead.", "warning" - ) + cx_print("āš ļø 'cortex template' is deprecated. Use 'cortex stack' instead.", "warning") if args.template_action == "list": cx_print(" Use: cortex stack list", "info") return cli.stack_list() From 9638c335fc8181268ba67ac31de153d3769fdb9a Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Fri, 26 Dec 2025 23:44:47 +0500 Subject: [PATCH 14/16] rerun SonarQube Cloud service --- cortex/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cortex/cli.py b/cortex/cli.py index 2d745706..9c1c50e9 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -2067,7 +2067,6 @@ def main(): if args.stack: return cli.install("", execute=args.execute, dry_run=args.dry_run, stack=args.stack) else: - # software is guaranteed to be set due to mutually_exclusive_group(required=True) return cli.install( args.software, execute=args.execute, From 946122090b6ae5c9f65641393fec0e36ddb2b0ee Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 7 Jan 2026 01:32:52 +0500 Subject: [PATCH 15/16] fix(templates): add sudo prefix and fix import validation for PR #201 --- cortex/cli.py | 22 ++++++++++++++-------- cortex/templates.py | 22 ++++++++++++++++++---- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/cortex/cli.py b/cortex/cli.py index 7f5198c9..bf0b3e4e 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -1089,14 +1089,20 @@ def _install_from_stack(self, stack_name: str, execute: bool, dry_run: bool): if execute: # Convert template steps to coordinator format if available if template.steps: - plan = [ - { - "command": step.command, - "description": step.description, - "rollback": step.rollback, - } - for step in template.steps - ] + plan = [] + for step in template.steps: + # Add sudo prefix if requires_root is True + command = step.command + if step.requires_root and not command.strip().startswith("sudo "): + command = f"sudo {command}" + + plan.append( + { + "command": command, + "description": step.description, + "rollback": step.rollback, + } + ) coordinator = InstallationCoordinator.from_plan( plan, timeout=300, stop_on_error=True ) diff --git a/cortex/templates.py b/cortex/templates.py index cafdeb23..2a371946 100644 --- a/cortex/templates.py +++ b/cortex/templates.py @@ -180,6 +180,11 @@ def _validate_post_install_commands(post_install: list[str]) -> list[str]: errors = [] for i, cmd in enumerate(post_install): + # Validate that cmd is a string (handle malformed data) + if not isinstance(cmd, str): + errors.append(f"post_install[{i}]: Must be a string, got {type(cmd).__name__}") + continue + if not cmd or not cmd.strip(): continue @@ -503,13 +508,22 @@ def generate_commands(self, template: Template) -> list[str]: Generate installation commands from template. Returns: - List of installation commands + List of installation commands with sudo prefix for root-required commands """ commands = [] # If template has explicit steps, use those if template.steps: - commands = [step.command for step in template.steps] + for step in template.steps: + # Add sudo prefix if requires_root is True + if step.requires_root: + # Don't add sudo if it's already there + if not step.command.strip().startswith("sudo "): + commands.append(f"sudo {step.command}") + else: + commands.append(step.command) + else: + commands.append(step.command) # Otherwise, generate from packages elif template.packages: # Use package manager to generate commands @@ -518,9 +532,9 @@ def generate_commands(self, template: Template) -> list[str]: try: commands = pm.parse(f"install {package_list}") except ValueError: - # Fallback: direct apt/yum install + # Fallback: direct apt/yum install with sudo pm_type = pm.pm_type.value - commands = [f"{pm_type} install -y {' '.join(template.packages)}"] + commands = [f"sudo {pm_type} install -y {' '.join(template.packages)}"] return commands From 35025531ff94419380ac98797c8e86ccc8e4e190 Mon Sep 17 00:00:00 2001 From: aliraza556 Date: Wed, 7 Jan 2026 01:44:09 +0500 Subject: [PATCH 16/16] fix(cli): remove non-existent user_preferences import blocking tests --- cortex/cli.py | 122 +++++++++----------------------------------------- 1 file changed, 21 insertions(+), 101 deletions(-) diff --git a/cortex/cli.py b/cortex/cli.py index bf0b3e4e..2149c210 100644 --- a/cortex/cli.py +++ b/cortex/cli.py @@ -25,11 +25,6 @@ from cortex.notification_manager import NotificationManager from cortex.stack_manager import StackManager from cortex.templates import InstallationStep, Template, TemplateFormat, TemplateManager -from cortex.user_preferences import ( - PreferencesManager, - format_preference_value, - print_all_preferences, -) from cortex.validators import ( validate_api_key, validate_install_request, @@ -47,7 +42,6 @@ def __init__(self, verbose: bool = False): self.spinner_chars = ["ā ‹", "ā ™", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ‡", "ā "] self.spinner_idx = 0 self.verbose = verbose - self.prefs_manager = None # Define a method to handle Docker-specific permission repairs def docker_permissions(self, args: argparse.Namespace) -> int: @@ -1386,97 +1380,22 @@ def stack_export(self, name: str, file_path: str, format: str = "yaml"): self._print_error(f"Failed to export stack: {str(e)}") return 1 - def _get_prefs_manager(self): - """Lazy initialize preferences manager""" - if self.prefs_manager is None: - self.prefs_manager = PreferencesManager() - return self.prefs_manager - - def check_pref(self, key: str | None = None): - """Check/display user preferences""" - manager = self._get_prefs_manager() - - try: - if key: - # Show specific preference - value = manager.get(key) - if value is None: - self._print_error(f"Preference key '{key}' not found") - return 1 - - print(f"\n{key} = {format_preference_value(value)}") - return 0 - else: - # Show all preferences - print_all_preferences(manager) - return 0 - - except (ValueError, OSError) as e: - self._print_error(f"Failed to read preferences: {str(e)}") - return 1 - except Exception as e: - self._print_error(f"Unexpected error reading preferences: {str(e)}") - if self.verbose: - import traceback - - traceback.print_exc() - return 1 - - def edit_pref(self, action: str, key: str | None = None, value: str | None = None): - """Edit user preferences (add/set, delete/remove, list)""" - manager = self._get_prefs_manager() - - try: - if action in ["add", "set", "update"]: - if not key or not value: - self._print_error("Key and value required") - return 1 - manager.set(key, value) - self._print_success(f"Updated {key}") - print(f" New value: {format_preference_value(manager.get(key))}") - return 0 - - elif action in ["delete", "remove", "reset-key"]: - if not key: - self._print_error("Key required") - return 1 - # Simplified reset logic - print(f"Resetting {key}...") - # (In a real implementation we would reset to default) - return 0 - - elif action in ["list", "show", "display"]: - return self.check_pref() - - elif action == "reset-all": - confirm = input("āš ļø Reset ALL preferences? (y/n): ") - if confirm.lower() == "y": - manager.reset() - self._print_success("Preferences reset") - return 0 - - elif action == "validate": - errors = manager.validate() - if errors: - print("āŒ Errors found") - else: - self._print_success("Valid") - return 0 - - else: - self._print_error(f"Unknown action: {action}") - return 1 - - except (ValueError, OSError) as e: - self._print_error(f"Failed to edit preferences: {str(e)}") - return 1 - except Exception as e: - self._print_error(f"Unexpected error editing preferences: {str(e)}") - if self.verbose: - import traceback - - traceback.print_exc() - return 1 + # NOTE: User preferences module not yet implemented + # def _get_prefs_manager(self): + # """Lazy initialize preferences manager""" + # if self.prefs_manager is None: + # self.prefs_manager = PreferencesManager() + # return self.prefs_manager + # + # def check_pref(self, key: str | None = None): + # """Check/display user preferences""" + # manager = self._get_prefs_manager() + # ... + # + # def edit_pref(self, action: str, key: str | None = None, value: str | None = None): + # """Edit user preferences (add/set, delete/remove, list)""" + # manager = self._get_prefs_manager() + # ... def status(self): """Show comprehensive system status and run health checks""" @@ -2724,10 +2643,11 @@ def main(): cx_print(" Use: cortex stack --help", "info") parser.print_help() return 1 - elif args.command == "check-pref": - return cli.check_pref(key=args.key) - elif args.command == "edit-pref": - return cli.edit_pref(action=args.action, key=args.key, value=args.value) + # NOTE: User preferences commands not yet implemented + # elif args.command == "check-pref": + # return cli.check_pref(key=args.key) + # elif args.command == "edit-pref": + # return cli.edit_pref(action=args.action, key=args.key, value=args.value) # Handle the new notify command elif args.command == "notify": return cli.notify(args)