Skip to content
Closed
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
63 changes: 63 additions & 0 deletions cortex/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import sys
import time
from datetime import datetime
from pathlib import Path
from typing import Any

from cortex.api_key_detector import auto_detect_api_key, setup_api_key
from cortex.ask import AskHandler
from cortex.branding import VERSION, console, cx_header, cx_print, show_banner
from cortex.config.git_manager import GitManager
from cortex.coordinator import InstallationCoordinator, InstallationStep, StepStatus
from cortex.demo import run_demo
from cortex.dependency_importer import (
Expand Down Expand Up @@ -1715,6 +1717,30 @@ def main():
parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed output")

subparsers = parser.add_subparsers(dest="command", help="Available commands")
# --------------------------
# Config command (Git-backed configuration management)
config_parser = subparsers.add_parser("config", help="Manage system configuration")

config_subparsers = config_parser.add_subparsers(dest="subcommand", help="Config subcommands")
config_subparsers.add_parser("history", help="Show configuration change history")
# config rollback
rollback_parser = config_subparsers.add_parser(
"rollback", help="Rollback configuration to a specific commit"
)
rollback_parser.add_argument("commit", help="Git commit hash")

# config git
config_git_parser = config_subparsers.add_parser("git", help="Git operations for configuration")

config_git_subparsers = config_git_parser.add_subparsers(dest="action", help="Git actions")

# config git init
config_git_subparsers.add_parser("init", help="Initialize git repository for configs")
# config git commit
commit_parser = config_git_subparsers.add_parser("commit", help="Commit configuration changes")
commit_parser.add_argument("message", help="Commit message")

# --------------------------

# Define the docker command and its associated sub-actions
docker_parser = subparsers.add_parser("docker", help="Docker and container utilities")
Expand Down Expand Up @@ -1997,6 +2023,43 @@ def main():
return cli.wizard()
elif args.command == "status":
return cli.status()
elif args.command == "config":
config_dir = Path.home() / ".cortex" / "configs"
config_dir.mkdir(parents=True, exist_ok=True)

gm = GitManager(str(config_dir))

if args.subcommand == "git" and args.action == "init":
created = gm.init_repo()
if created:
print("✓ Git repository initialized")
else:
print("✓ Git repository already exists")
return 0

if args.subcommand == "git" and args.action == "commit":
committed = gm.commit_all(args.message)
if committed:
print(f'✓ Auto-committed: "{args.message}"')
else:
print("ℹ No changes to commit")
return 0

if args.subcommand == "history":
output = gm.history()
if output:
print(output)
else:
print("No configuration history found")
return 0
if args.subcommand == "rollback":
gm.rollback(args.commit)
print(f"✓ Rolled back to commit {args.commit}")
return 0

print("Unknown config command")
return 1

elif args.command == "ask":
return cli.ask(args.question)
elif args.command == "install":
Expand Down
Empty file added cortex/config/__init__.py
Empty file.
52 changes: 52 additions & 0 deletions cortex/config/git_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import subprocess
from pathlib import Path


class GitManager:
def __init__(self, config_path: str):
self.config_path = Path(config_path)

def init_repo(self):
if (self.config_path / ".git").exists():
return False

subprocess.run(["git", "init"], cwd=self.config_path, check=True)
return True

def commit_all(self, message: str) -> bool:
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing blank line before method definition. According to PEP 8, there should be two blank lines between top-level function and class definitions, and one blank line between method definitions within a class. This method is missing the blank line separation from the previous method.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subprocess.run(["git", "add", "."], cwd=self.config_path, check=True)

result = subprocess.run(
["git", "commit", "-m", message],
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential command injection vulnerability. The message parameter is passed directly to subprocess without sanitization. While passing it as a list argument provides some protection, special characters or escape sequences in the commit message could still cause issues. Consider validating or sanitizing the message parameter.

Copilot uses AI. Check for mistakes.
cwd=self.config_path,
capture_output=True,
text=True,
check=False,
)

if "nothing to commit" in result.stdout.lower():
return False

if result.returncode != 0:
raise RuntimeError(result.stderr.strip())

return True

def history(self) -> str:
result = subprocess.run(
["git", "log", "--oneline", "--relative-date"],
cwd=self.config_path,
capture_output=True,
text=True,
)

if result.returncode != 0:
# No commits yet
return ""

return result.stdout.strip()

def rollback(self, commit_hash: str) -> None:
subprocess.run(
["git", "checkout", commit_hash, "--", "."], cwd=self.config_path, check=True
)
47 changes: 47 additions & 0 deletions tests/test_git_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from pathlib import Path
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused import. The Path class is imported but never used in the test file. The tmp_path fixture already provides Path objects, and string conversions are done inline without using the Path constructor.

Suggested change
from pathlib import Path

Copilot uses AI. Check for mistakes.
from unittest.mock import patch

from cortex.config.git_manager import GitManager


@patch("subprocess.run")
def test_init_repo_creates_git(mock_run, tmp_path):
gm = GitManager(str(tmp_path))
assert gm.init_repo() is True


@patch("subprocess.run")
def test_init_repo_when_exists(mock_run, tmp_path):
(tmp_path / ".git").mkdir()
gm = GitManager(str(tmp_path))
assert gm.init_repo() is False


@patch("subprocess.run")
def test_commit_all_success(mock_run, tmp_path):
mock_run.return_value.returncode = 0
mock_run.return_value.stdout = "Committed"
gm = GitManager(str(tmp_path))
assert gm.commit_all("Test commit") is True


@patch("subprocess.run")
def test_commit_all_no_changes(mock_run, tmp_path):
mock_run.return_value.returncode = 1
mock_run.return_value.stdout = "nothing to commit"
gm = GitManager(str(tmp_path))
assert gm.commit_all("No changes") is False


@patch("subprocess.run")
def test_history_empty(mock_run, tmp_path):
mock_run.return_value.returncode = 1
gm = GitManager(str(tmp_path))
assert gm.history() == ""


@patch("subprocess.run")
def test_rollback(mock_run, tmp_path):
gm = GitManager(str(tmp_path))
gm.rollback("abc123")
mock_run.assert_called()
Comment on lines +7 to +47
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect mock patch location. The patch decorator should target "cortex.config.git_manager.subprocess.run" instead of just "subprocess.run" to properly mock the subprocess module as imported in the GitManager module. The current patch may not work correctly depending on how the module is imported.

Copilot uses AI. Check for mistakes.
Comment on lines +43 to +47
Copy link

Copilot AI Dec 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incomplete test assertions. The test only verifies that subprocess.run was called, but doesn't verify that it was called with the correct arguments (git checkout command with the specific commit hash). This makes the test less effective at catching regressions in the rollback functionality.

Copilot uses AI. Check for mistakes.
Loading