Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,7 @@ dmypy.json
.codesage/
report.md
report.json
codesage.db
plan.yml
task.yml
enriched_snapshot.yml
Binary file removed codesage.db
Binary file not shown.
5 changes: 3 additions & 2 deletions codesage/cli/commands/diff.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
@click.command()
@click.argument('version1')
@click.argument('version2')
@click.option('--project', '-p', required=True, help='The name of the project.')
@click.option('--output', '-o', type=click.Path(), help='Output file for the diff report.')
@click.option('--format', '-f', type=click.Choice(['json', 'markdown']), default='json', help='Output format.')
def diff(version1, version2, output, format):
def diff(version1, version2, project, output, format):
"""
Compare two snapshots and show the differences.
"""
manager = SnapshotVersionManager(SNAPSHOT_DIR, DEFAULT_CONFIG['snapshot'])
manager = SnapshotVersionManager(SNAPSHOT_DIR, project, DEFAULT_CONFIG['snapshot'])

from codesage.snapshot.models import ProjectSnapshot
import json
Expand Down
33 changes: 21 additions & 12 deletions codesage/cli/commands/governance_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@
from codesage.audit.models import AuditEvent
from datetime import datetime

from codesage.snapshot.versioning import SnapshotVersionManager
from codesage.config.defaults import SNAPSHOT_DIR, DEFAULT_SNAPSHOT_CONFIG

@click.command(name="governance-plan", help="Generate a governance plan from a project snapshot.")
@click.option(
"--input",
"input_path",
"--snapshot-version",
"snapshot_version",
required=True,
help="The version of the snapshot to use.",
)
@click.option(
"--project",
"project_name",
required=True,
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
help="Path to the input project snapshot YAML file.",
help="The name of the project.",
)
@click.option(
"--output",
Expand All @@ -30,21 +38,22 @@
@click.pass_context
def governance_plan(
ctx,
input_path: str,
snapshot_version: str,
project_name: str,
output_path: str,
group_by: str | None,
max_tasks_per_file: int | None,
):
"""
Generates a governance plan from a project snapshot YAML file.
Generates a governance plan from a project snapshot.
"""
audit_logger = ctx.obj.audit_logger
project_name = None
try:
click.echo(f"Loading snapshot from {input_path}...")
snapshot_data = read_yaml_file(Path(input_path))
snapshot = ProjectSnapshot.model_validate(snapshot_data)
project_name = snapshot.metadata.project_name
manager = SnapshotVersionManager(SNAPSHOT_DIR, project_name, DEFAULT_SNAPSHOT_CONFIG['snapshot'])
snapshot = manager.load_snapshot(snapshot_version)
if not snapshot:
click.echo(f"Snapshot {snapshot_version} not found for project '{project_name}'.", err=True)
return

# Apply config overrides
config = GovernanceConfig.default()
Expand Down Expand Up @@ -74,7 +83,7 @@ def governance_plan(
project_name=project_name,
command="governance-plan",
args={
"input_path": input_path,
"snapshot_version": snapshot_version,
"output_path": output_path,
"group_by": group_by,
"max_tasks_per_file": max_tasks_per_file,
Expand Down
35 changes: 28 additions & 7 deletions codesage/cli/commands/jules_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@
from codesage.audit.models import AuditEvent
from datetime import datetime

from codesage.snapshot.versioning import SnapshotVersionManager
from codesage.config.defaults import SNAPSHOT_DIR, DEFAULT_SNAPSHOT_CONFIG

@click.command('jules-prompt', help="Generate a Jules prompt for a specific governance task.")
@click.option('--plan', 'plan_path', type=click.Path(exists=True), help="Path to the governance_plan.yaml file.")
@click.option('--task-id', help="The ID of the task within the governance plan.")
@click.option('--task', 'task_path', type=click.Path(exists=True), help="Path to a single GovernanceTask YAML/JSON file.")
@click.option('--project', '-p', 'project_name', type=str, help='The name of the project.')
@click.option('--snapshot-version', '-s', 'snapshot_version', type=str, help='The version of the snapshot to use.')
@click.pass_context
def jules_prompt(ctx, plan_path: Optional[str], task_id: Optional[str], task_path: Optional[str]):
def jules_prompt(ctx, plan_path: Optional[str], task_id: Optional[str], task_path: Optional[str], project_name: Optional[str], snapshot_version: Optional[str]):
"""
Generates a prompt for Jules from a governance task.
"""
Expand Down Expand Up @@ -60,18 +65,32 @@ def jules_prompt(ctx, plan_path: Optional[str], task_id: Optional[str], task_pat
click.echo(f"Error: Template with ID '{recipe.template_id}' not found.", err=True)
return

# The JulesTaskView requires a code snippet, which we don't have in the GovernanceTask alone.
# For this CLI tool, we'll use a placeholder. A more advanced implementation
# would need access to the project snapshot to get the real code.
# We will use the `llm_hint` field from the task metadata if it exists.
snapshot = None
if project_name and snapshot_version:
manager = SnapshotVersionManager(SNAPSHOT_DIR, project_name, DEFAULT_SNAPSHOT_CONFIG['snapshot'])
snapshot = manager.load_snapshot(snapshot_version)
if not snapshot:
click.echo(f"Warning: Snapshot {snapshot_version} not found for project '{project_name}'. Code snippet will not be available.", err=True)

code_snippet_placeholder = task.metadata.get("code_snippet", "[Code snippet not available in this context. Please refer to the file and line number.]")
code_snippet = "[Code snippet not available in this context. Please refer to the file and line number.]"
if snapshot:
for file_snapshot in snapshot.files:
if file_snapshot.path == task.file_path:
try:
with open(file_snapshot.path, 'r', encoding='utf-8') as f:
lines = f.readlines()
start_line = task.metadata.get("start_line", 1) - 1
end_line = task.metadata.get("end_line", start_line + 10)
code_snippet = "".join(lines[start_line:end_line])
except Exception as e:
click.echo(f"Warning: Could not read file {file_snapshot.path}: {e}", err=True)
break

# Create a simple JulesTaskView from the task
view = JulesTaskView(
file_path=task.file_path,
language=task.language,
code_snippet=code_snippet_placeholder,
code_snippet=code_snippet,
issue_message=task.description,
goal_description=task.metadata.get("goal_description", "Fix the issue described above."),
line=task.metadata.get("start_line"),
Expand All @@ -94,6 +113,8 @@ def jules_prompt(ctx, plan_path: Optional[str], task_id: Optional[str], task_pat
"plan_path": plan_path,
"task_id": task_id,
"task_path": task_path,
"project_name": project_name,
"snapshot_version": snapshot_version,
},
)
)
17 changes: 11 additions & 6 deletions codesage/cli/commands/llm_suggest.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,23 @@
from codesage.snapshot.yaml_generator import YAMLGenerator


from codesage.snapshot.versioning import SnapshotVersionManager
from codesage.config.defaults import SNAPSHOT_DIR, DEFAULT_SNAPSHOT_CONFIG

@click.command('llm-suggest')
@click.option('--input', '-i', 'input_path', type=click.Path(exists=True, dir_okay=False), required=True, help='Input snapshot YAML file.')
@click.option('--snapshot-version', '-s', 'snapshot_version', type=str, required=True, help='The version of the snapshot to use.')
@click.option('--project', '-p', 'project_name', type=str, required=True, help='The name of the project.')
@click.option('--output', '-o', 'output_path', type=click.Path(), required=True, help='Output snapshot YAML file.')
@click.option('--provider', type=click.Choice(['dummy']), default='dummy', help='LLM provider to use.')
@click.option('--model', type=str, default='dummy-model', help='LLM model to use.')
def llm_suggest(input_path, output_path, provider, model):
def llm_suggest(snapshot_version, project_name, output_path, provider, model):
"""Enrich a snapshot with LLM-powered suggestions."""

with open(input_path, 'r') as f:
snapshot_data = yaml.safe_load(f)

project_snapshot = ProjectSnapshot.model_validate(snapshot_data)
manager = SnapshotVersionManager(SNAPSHOT_DIR, project_name, DEFAULT_SNAPSHOT_CONFIG['snapshot'])
project_snapshot = manager.load_snapshot(snapshot_version)
if not project_snapshot:
click.echo(f"Snapshot {snapshot_version} not found for project '{project_name}'.", err=True)
return

if provider == 'dummy':
llm_client = DummyLLMClient()
Expand Down
78 changes: 41 additions & 37 deletions codesage/cli/commands/snapshot.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import json
import gzip
import hashlib
from datetime import datetime
from datetime import datetime, timezone
from pathlib import Path

from codesage.snapshot.versioning import SnapshotVersionManager
Expand Down Expand Up @@ -46,7 +46,7 @@ def snapshot():
from codesage.audit.models import AuditEvent


def _create_snapshot_data(path):
def _create_snapshot_data(path, project_name):
file_snapshots = []
for root, dirs, files in os.walk(path):
dirs[:] = [d for d in dirs if d not in DEFAULT_EXCLUDE_DIRS]
Expand Down Expand Up @@ -81,7 +81,7 @@ def _create_snapshot_data(path):
metadata=SnapshotMetadata(
version="",
timestamp=datetime.now(),
project_name=os.path.basename(os.path.abspath(path)),
project_name=project_name,
file_count=len(file_snapshots),
total_size=total_size,
tool_version=tool_version,
Expand All @@ -97,15 +97,16 @@ def _create_snapshot_data(path):

@snapshot.command('create')
@click.argument('path', type=click.Path(exists=True, dir_okay=True))
@click.option('--project', '-p', 'project_name_override', help='Override the project name.')
@click.option('--format', '-f', type=click.Choice(['json', 'python-semantic-digest']), default='json', help='Snapshot format.')
@click.option('--output', '-o', type=click.Path(), default=None, help='Output file path.')
@click.option('--compress', is_flag=True, help='Enable compression.')
@click.option('--language', '-l', type=click.Choice(['python', 'go', 'shell', 'java', 'auto']), default='auto', help='Language to analyze.')
@click.pass_context
def create(ctx, path, format, output, compress, language):
def create(ctx, path, project_name_override, format, output, compress, language):
"""Create a new snapshot from the given path."""
audit_logger = ctx.obj.audit_logger
project_name = os.path.basename(os.path.abspath(path))
project_name = project_name_override or os.path.basename(os.path.abspath(path))
try:
root_path = Path(path)

Expand Down Expand Up @@ -168,31 +169,38 @@ def create(ctx, path, format, output, compress, language):
click.echo(f"{language.capitalize()} semantic digest created at {output}")
return

snapshot_data = _create_snapshot_data(path)
snapshot_data = _create_snapshot_data(path, project_name)

if output:
output_path = Path(output)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
# Use model_dump_json for consistency
with open(output_path, 'w', encoding='utf-8') as f:
f.write(snapshot_data.model_dump_json(indent=2))

click.echo(f"Snapshot created at {output}")
else:
manager = SnapshotVersionManager(SNAPSHOT_DIR, DEFAULT_SNAPSHOT_CONFIG['snapshot'])
manager = SnapshotVersionManager(SNAPSHOT_DIR, project_name, DEFAULT_SNAPSHOT_CONFIG['snapshot'])

# The format for saving via manager is 'json', not the input format for semantic digests
save_format = 'json'

if compress:
snapshot_path = manager.save_snapshot(snapshot_data, format)
snapshot_path = manager.save_snapshot(snapshot_data, save_format)

# Compress the file
with open(snapshot_path, 'rb') as f_in:
with gzip.open(f"{snapshot_path}.gz", 'wb') as f_out:
f_out.writelines(f_in)
os.remove(snapshot_path)
click.echo(f"Compressed snapshot created at {snapshot_path}.gz")
else:
snapshot_path = manager.save_snapshot(snapshot_data, format)
snapshot_path = manager.save_snapshot(snapshot_data, save_format)
click.echo(f"Snapshot created at {snapshot_path}")
finally:
audit_logger.log(
AuditEvent(
timestamp=datetime.now(),
timestamp=datetime.now(timezone.utc),
event_type="cli.snapshot.create",
project_name=project_name,
command="snapshot create",
Expand All @@ -207,56 +215,52 @@ def create(ctx, path, format, output, compress, language):
)

@snapshot.command('list')
def list_snapshots():
"""List all available snapshots."""
manager = SnapshotVersionManager(SNAPSHOT_DIR, DEFAULT_SNAPSHOT_CONFIG['snapshot'])
@click.option('--project', '-p', required=True, help='The name of the project.')
def list_snapshots(project):
"""List all available snapshots for a project."""
manager = SnapshotVersionManager(SNAPSHOT_DIR, project, DEFAULT_SNAPSHOT_CONFIG['snapshot'])
snapshots = manager.list_snapshots()
if not snapshots:
click.echo("No snapshots found.")
click.echo(f"No snapshots found for project '{project}'.")
return
for s in snapshots:
click.echo(f"- {s['version']} ({s['timestamp']})")

@snapshot.command('show')
@click.argument('version')
def show(version):
@click.option('--project', '-p', required=True, help='The name of the project.')
def show(version, project):
"""Show details of a specific snapshot."""
manager = SnapshotVersionManager(SNAPSHOT_DIR, DEFAULT_SNAPSHOT_CONFIG['snapshot'])
manager = SnapshotVersionManager(SNAPSHOT_DIR, project, DEFAULT_SNAPSHOT_CONFIG['snapshot'])
snapshot_data = manager.load_snapshot(version)
if not snapshot_data:
click.echo(f"Snapshot {version} not found.", err=True)
click.echo(f"Snapshot {version} not found for project '{project}'.", err=True)
return
click.echo(snapshot_data.model_dump_json(indent=2))

@snapshot.command('cleanup')
@click.option('--project', '-p', required=True, help='The name of the project.')
@click.option('--dry-run', is_flag=True, help='Show which snapshots would be deleted.')
def cleanup(dry_run):
"""Clean up old snapshots."""
from datetime import timedelta

manager = SnapshotVersionManager(SNAPSHOT_DIR, DEFAULT_SNAPSHOT_CONFIG['snapshot'])
def cleanup(project, dry_run):
"""Clean up old snapshots for a project."""
manager = SnapshotVersionManager(SNAPSHOT_DIR, project, DEFAULT_SNAPSHOT_CONFIG['snapshot'])

if dry_run:
index = manager._load_index()
now = datetime.now()

expired_by_date = [
s for s in index
if now - datetime.fromisoformat(s["timestamp"]) > timedelta(days=manager.retention_days)
]

sorted_by_date = sorted(index, key=lambda s: s["timestamp"], reverse=True)
expired_by_count = sorted_by_date[manager.max_versions:]
if not index:
click.echo(f"No snapshots to clean up for project '{project}'.")
return

expired = {s['version']: s for s in expired_by_date + expired_by_count}.values()
now = datetime.now(timezone.utc)
expired_snapshots = manager._get_expired_snapshots(index, now)

if not expired:
click.echo("No snapshots to clean up.")
if not expired_snapshots:
click.echo(f"No snapshots to clean up for project '{project}'.")
return

click.echo("Snapshots to be deleted:")
for s in expired:
for s in expired_snapshots:
click.echo(f"- {s['version']}")
else:
manager.cleanup_expired_snapshots()
click.echo("Expired snapshots have been cleaned up.")
click.echo(f"Expired snapshots for project '{project}' have been cleaned up.")
Loading
Loading