Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 0 additions & 43 deletions .github/workflows/codeql.yml

This file was deleted.

217 changes: 159 additions & 58 deletions cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import argparse
import time
import logging
import shutil
import traceback
import urllib.request
from typing import List, Optional
from datetime import datetime

Expand Down Expand Up @@ -34,9 +37,7 @@
)
from cortex.validators import (
validate_api_key,
validate_install_request,
validate_installation_id,
ValidationError
validate_install_request
)
# Import the new Notification Manager
from cortex.notification_manager import NotificationManager
Expand All @@ -63,7 +64,8 @@

is_valid, detected_provider, error = validate_api_key()
if not is_valid:
self._print_error(error)
if error:
self._print_error(error)
cx_print("Run [bold]cortex wizard[/bold] to configure your API key.", "info")
cx_print("Or use [bold]CORTEX_PROVIDER=ollama[/bold] for offline mode.", "info")
return None
Expand Down Expand Up @@ -176,11 +178,12 @@
return 1
# -------------------------------

def install(self, software: str, execute: bool = False, dry_run: bool = False):
def install(self, software: str, execute: bool = False, dry_run: bool = False, parallel: bool = False):

Check failure on line 181 in cortex/cli.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this function to reduce its Cognitive Complexity from 101 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsIhAKmNR_PctvIblpb&open=AZsIhAKmNR_PctvIblpb&pullRequest=288
# Validate input first
is_valid, error = validate_install_request(software)
if not is_valid:
self._print_error(error)
if error:
self._print_error(error)
return 1

api_key = self._get_api_key()
Expand Down Expand Up @@ -247,48 +250,120 @@
print(f" Command: {step.command}")

print("\nExecuting commands...")

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
)

result = coordinator.execute()

if result.success:
self._print_success(f"{software} installed successfully!")
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

if parallel:
# Use parallel execution
import asyncio
from cortex.install_parallel import run_parallel_install

def parallel_log_callback(message: str, level: str = "info"):
"""Callback for parallel execution logging."""
if level == "success":
cx_print(f" ✅ {message}", "success")
elif level == "error":
cx_print(f" ❌ {message}", "error")
else:
cx_print(f" ℹ {message}", "info")

try:
success, parallel_tasks = asyncio.run(
run_parallel_install(
commands=commands,
descriptions=[f"Step {i+1}" for i in range(len(commands))],
timeout=300,
stop_on_error=True,
log_callback=parallel_log_callback
)
)

if result.failed_step is not None:
self._print_error(f"Installation failed at step {result.failed_step + 1}")

# Calculate total duration from tasks
total_duration = 0
if parallel_tasks:
max_end = max((t.end_time for t in parallel_tasks if t.end_time is not None), default=None)
min_start = min((t.start_time for t in parallel_tasks if t.start_time is not None), default=None)
if max_end is not None and min_start is not None:
total_duration = max_end - min_start

if success:
self._print_success(f"{software} installed successfully!")
print(f"\nCompleted in {total_duration:.2f} seconds (parallel mode)")

# 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:
# Find failed task for error reporting
failed_tasks = [t for t in parallel_tasks if t.status.value == "failed"]
error_msg = failed_tasks[0].error if failed_tasks else "Installation failed"

Check failure on line 301 in cortex/cli.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Define a constant instead of duplicating this literal "Installation failed" 4 times.

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsIhAKmNR_PctvIblpa&open=AZsIhAKmNR_PctvIblpa&pullRequest=288

if install_id:
history.update_installation(
install_id,
InstallationStatus.FAILED,
error_msg
)

self._print_error("Installation failed")
if error_msg:
print(f" Error: {error_msg}", 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

except Exception as e:
if install_id:
history.update_installation(install_id, InstallationStatus.FAILED, str(e))
self._print_error(f"Parallel execution failed: {str(e)}")
return 1

else:
# Use sequential execution (original behavior)
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
)

result = coordinator.execute()

if result.success:
self._print_success(f"{software} installed successfully!")
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:
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
# 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("Example: cortex install docker --execute")
Expand Down Expand Up @@ -467,12 +542,11 @@

except Exception as e:
self._print_error(f"Failed to edit preferences: {str(e)}")
traceback.print_exc()
return 1

def status(self):
"""Show system status including security features"""
import shutil

show_banner(show_version=True)
console.print()

Expand All @@ -499,6 +573,32 @@
cx_print("Firejail: [bold]Not installed[/bold]", "warning")
cx_print(" Install: sudo apt-get install firejail", "info")

# Check Ollama
ollama_host = os.environ.get('OLLAMA_HOST', 'http://localhost:11434')
try:
req = urllib.request.Request(f"{ollama_host}/api/tags", method='GET')
with urllib.request.urlopen(req, timeout=2) as resp:

Check warning on line 580 in cortex/cli.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unused local variable "resp".

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsNK1Y5fMeRJ4F2fFX-&open=AZsNK1Y5fMeRJ4F2fFX-&pullRequest=288
cx_print(f"Ollama: [bold]Running[/bold] ({ollama_host})", "success")
except Exception:
cx_print(f"Ollama: [bold]Not running[/bold]", "info")

Check warning on line 583 in cortex/cli.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add replacement fields or use a normal string instead of an f-string.

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsNK1Y5fMeRJ4F2fFX_&open=AZsNK1Y5fMeRJ4F2fFX_&pullRequest=288
cx_print(" Start: ollama serve", "info")

console.print()
cx_header("Configuration")

# Show config file location
config_path = os.path.expanduser('~/.cortex/config.json')
if os.path.exists(config_path):
cx_print(f"Config: {config_path}", "info")
else:
cx_print(f"Config: Not created yet", "info")

Check warning on line 594 in cortex/cli.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add replacement fields or use a normal string instead of an f-string.

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsNK1Y5fMeRJ4F2fFYA&open=AZsNK1Y5fMeRJ4F2fFYA&pullRequest=288

history_path = os.path.expanduser('~/.cortex/history.db')
if os.path.exists(history_path):
cx_print(f"History: {history_path}", "info")
else:
cx_print(f"History: Not created yet", "info")

Check warning on line 600 in cortex/cli.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add replacement fields or use a normal string instead of an f-string.

See more on https://sonarcloud.io/project/issues?id=cortexlinux_cortex&issues=AZsNK1Y5fMeRJ4F2fFYB&open=AZsNK1Y5fMeRJ4F2fFYB&pullRequest=288

console.print()
return 0

Expand Down Expand Up @@ -564,19 +664,20 @@
subparsers = parser.add_subparsers(dest='command', help='Available commands')

# Demo command
demo_parser = subparsers.add_parser('demo', help='See Cortex in action')
_demo_parser = subparsers.add_parser('demo', help='See Cortex in action')

# Wizard command
wizard_parser = subparsers.add_parser('wizard', help='Configure API key interactively')
_wizard_parser = subparsers.add_parser('wizard', help='Configure API key interactively')

# Status command
status_parser = subparsers.add_parser('status', help='Show system status')
_status_parser = subparsers.add_parser('status', help='Show system status')

# Install command
install_parser = subparsers.add_parser('install', help='Install software')
install_parser.add_argument('software', type=str, help='Software to install')
install_parser.add_argument('--execute', action='store_true', help='Execute commands')
install_parser.add_argument('--dry-run', action='store_true', help='Show commands only')
install_parser.add_argument('--parallel', action='store_true', help='Execute independent tasks in parallel')

# History command
history_parser = subparsers.add_parser('history', help='View history')
Expand All @@ -590,13 +691,13 @@
rollback_parser.add_argument('--dry-run', action='store_true')

# Preferences commands
check_pref_parser = subparsers.add_parser('check-pref', help='Check preferences')
check_pref_parser.add_argument('key', nargs='?')
_check_pref_parser = subparsers.add_parser('check-pref', help='Check preferences')
_check_pref_parser.add_argument('key', nargs='?')

edit_pref_parser = subparsers.add_parser('edit-pref', help='Edit preferences')
edit_pref_parser.add_argument('action', choices=['set', 'add', 'delete', 'list', 'validate'])
edit_pref_parser.add_argument('key', nargs='?')
edit_pref_parser.add_argument('value', nargs='?')
_edit_pref_parser = subparsers.add_parser('edit-pref', help='Edit preferences')
_edit_pref_parser.add_argument('action', choices=['set', 'add', 'delete', 'list', 'validate'])
_edit_pref_parser.add_argument('key', nargs='?')
_edit_pref_parser.add_argument('value', nargs='?')

# --- New Notify Command ---
notify_parser = subparsers.add_parser('notify', help='Manage desktop notifications')
Expand Down Expand Up @@ -633,7 +734,7 @@
elif args.command == 'status':
return cli.status()
elif args.command == 'install':
return cli.install(args.software, execute=args.execute, dry_run=args.dry_run)
return cli.install(args.software, execute=args.execute, dry_run=args.dry_run, parallel=args.parallel)
elif args.command == 'history':
return cli.history(limit=args.limit, status=args.status, show_id=args.show_id)
elif args.command == 'rollback':
Expand Down
18 changes: 2 additions & 16 deletions cortex/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,9 @@
from datetime import datetime
import logging

logger = logging.getLogger(__name__)
from cortex.validators import DANGEROUS_PATTERNS

# Dangerous patterns that should never be executed
DANGEROUS_PATTERNS = [
r'rm\s+-rf\s+[/\*]',
r'rm\s+--no-preserve-root',
r'dd\s+if=.*of=/dev/',
r'curl\s+.*\|\s*sh',
r'curl\s+.*\|\s*bash',
r'wget\s+.*\|\s*sh',
r'wget\s+.*\|\s*bash',
r'\beval\s+',
r'base64\s+-d\s+.*\|',
r'>\s*/etc/',
r'chmod\s+777',
r'chmod\s+\+s',
]
logger = logging.getLogger(__name__)


class StepStatus(Enum):
Expand Down
Loading
Loading