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
182 changes: 181 additions & 1 deletion src/shelfai/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,17 @@
from typing import Optional

import typer
import yaml
from rich.console import Console
from rich.markup import escape as markup_escape
from rich.table import Table

from shelfai import __version__
from shelfai.core.config_schema import (
generate_config_template,
get_default_config,
validate_config_file,
)
from shelfai.core.display import error as display_error
from shelfai.core.display import header as display_header
from shelfai.core.display import info as display_info
Expand All @@ -50,6 +56,7 @@
help="📚 ShelfAI — Lightweight, file-based context management for AI agents.",
no_args_is_help=True,
)
config_app = typer.Typer(name="config", help="Manage chunk defaults and validation.")
console = Console()
console_err = Console(stderr=True)

Expand All @@ -68,11 +75,184 @@
},
}

app.add_typer(config_app)


def _load_chunk_defaults(start: Path, agent_dir: Optional[Path] = None):
from shelfai.core.config import ChunkDefaultsConfig

repo_root = ChunkDefaultsConfig.find_repo_root(start=start)
return ChunkDefaultsConfig.load(agent_dir=agent_dir, repo_root=repo_root)
config = ChunkDefaultsConfig.default()

if repo_root:
config = _merge_chunk_defaults_layer(
config,
repo_root / ".chunk-defaults.yaml",
label="repo",
)

if agent_dir:
config = _merge_chunk_defaults_layer(
config,
Path(agent_dir) / "chunk.yaml",
label="agent",
)

return config


def _merge_chunk_defaults_layer(base, path: Path, label: str):
from shelfai.core.config import ChunkDefaultsConfig

if not path.exists():
return base

is_valid, errors = validate_config_file(str(path))
if not is_valid:
_warn_invalid_chunk_defaults(path, label, errors)
return base

try:
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except Exception as exc:
_warn_invalid_chunk_defaults(path, label, [f"Failed to load YAML: {exc}"])
return base

try:
return ChunkDefaultsConfig._merge(base, raw)
except Exception as exc:
_warn_invalid_chunk_defaults(path, label, [f"Failed to merge chunk defaults: {exc}"])
return base


def _warn_invalid_chunk_defaults(path: Path, label: str, errors: list[str]) -> None:
console_err.print(
f"[yellow]Warning: invalid {label} chunk defaults at {path}. "
"Using built-in defaults for that layer.[/yellow]"
)
for error in errors:
console_err.print(f"[yellow] - {error}[/yellow]")


def _load_effective_chunk_defaults_dict(start: Path, agent_dir: Optional[Path] = None) -> dict:
from shelfai.core.config import ChunkDefaultsConfig

repo_root = ChunkDefaultsConfig.find_repo_root(start=start)
config = get_default_config()

if repo_root:
config = _merge_raw_chunk_defaults_layer(
config,
repo_root / ".chunk-defaults.yaml",
label="repo",
)

if agent_dir:
config = _merge_raw_chunk_defaults_layer(
config,
Path(agent_dir) / "chunk.yaml",
label="agent",
)

return config


def _merge_raw_chunk_defaults_layer(base: dict, path: Path, label: str) -> dict:
if not path.exists():
return base

is_valid, errors = validate_config_file(str(path))
if not is_valid:
_warn_invalid_chunk_defaults(path, label, errors)
return base

try:
raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except Exception as exc:
_warn_invalid_chunk_defaults(path, label, [f"Failed to load YAML: {exc}"])
return base

return _deep_merge_dicts(base, raw)


def _deep_merge_dicts(base: dict, overrides: dict) -> dict:
merged = dict(base)
for key, value in overrides.items():
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
merged[key] = _deep_merge_dicts(merged[key], value)
else:
merged[key] = value
return merged


def _resolve_chunk_defaults_path(path: Optional[str]) -> Path:
resolved = Path(path).expanduser() if path else Path.cwd() / ".chunk-defaults.yaml"
if resolved.is_dir():
resolved = resolved / ".chunk-defaults.yaml"
return resolved.resolve()


def _resolve_chunk_defaults_context(path: Optional[str]) -> tuple[Path, Optional[Path]]:
if path is None:
return Path.cwd(), None

resolved = Path(path).expanduser().resolve()
if resolved.is_dir():
return resolved, None
if resolved.name == "chunk.yaml":
return resolved.parent, resolved.parent
if resolved.name == ".chunk-defaults.yaml":
return resolved.parent, None
return resolved.parent, None


@config_app.command("validate")
def config_validate(
path: Optional[str] = typer.Argument(
None, help="Path to .chunk-defaults.yaml or a directory containing it"
),
):
"""Validate a chunk defaults file."""
config_path = _resolve_chunk_defaults_path(path)
is_valid, errors = validate_config_file(str(config_path))

if is_valid:
console.print(f"[green]Valid config:[/green] {config_path}")
return

console.print(f"[red]Invalid config:[/red] {config_path}")
for error in errors:
console.print(f" - {error}")
raise typer.Exit(1)


@config_app.command("init")
def config_init(
path: Optional[str] = typer.Argument(
None, help="Directory or file path where .chunk-defaults.yaml should be created"
),
):
"""Generate a commented .chunk-defaults.yaml template."""
config_path = _resolve_chunk_defaults_path(path)
if config_path.exists():
console.print(f"[yellow]Config already exists at {config_path}[/yellow]")
raise typer.Exit(1)

config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(generate_config_template(), encoding="utf-8")
console.print(f"[green]Wrote template config to {config_path}[/green]")


@config_app.command("show")
def config_show(
path: Optional[str] = typer.Argument(
None, help="Directory or config file to use as the override source"
),
):
"""Show the effective chunk config."""
start, agent_dir = _resolve_chunk_defaults_context(path)
effective = _load_effective_chunk_defaults_dict(start=start, agent_dir=agent_dir)
console.print(yaml.safe_dump(effective, sort_keys=False, default_flow_style=False))


def _chunk_version_store(shelf_root: Path):
Expand Down
Loading
Loading