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,497 changes: 33 additions & 4,464 deletions src/apm_cli/cli.py

Large diffs are not rendered by default.

405 changes: 405 additions & 0 deletions src/apm_cli/commands/_helpers.py

Large diffs are not rendered by default.

739 changes: 739 additions & 0 deletions src/apm_cli/commands/compile.py

Large diffs are not rendered by default.

169 changes: 169 additions & 0 deletions src/apm_cli/commands/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
"""APM config command group."""

import builtins
import sys
from pathlib import Path

import click

from ..utils.console import _rich_echo, _rich_error, _rich_info, _rich_success
from ..version import get_version
from ._helpers import HIGHLIGHT, RESET, _get_console, _load_apm_config

# Restore builtin since a subcommand is named ``set``
set = builtins.set


@click.group(help="Configure APM CLI", invoke_without_command=True)
@click.pass_context
def config(ctx):
"""Configure APM CLI settings."""
# If no subcommand, show current configuration
if ctx.invoked_subcommand is None:
try:
# Lazy import rich table
from rich.table import Table # type: ignore

console = _get_console()
# Create configuration display
config_table = Table(
title="⚙️ Current APM Configuration",
show_header=True,
header_style="bold cyan",
)
config_table.add_column("Category", style="bold yellow", min_width=12)
config_table.add_column("Setting", style="white", min_width=15)
config_table.add_column("Value", style="cyan")

# Show apm.yml if in project
if Path("apm.yml").exists():
apm_config = _load_apm_config()
config_table.add_row(
"Project", "Name", apm_config.get("name", "Unknown")
)
config_table.add_row(
"", "Version", apm_config.get("version", "Unknown")
)
config_table.add_row(
"", "Entrypoint", apm_config.get("entrypoint", "None")
)
config_table.add_row(
"",
"MCP Dependencies",
str(len(apm_config.get("dependencies", {}).get("mcp", []))),
)

# Show compilation configuration
compilation_config = apm_config.get("compilation", {})
if compilation_config:
config_table.add_row(
"Compilation",
"Output",
compilation_config.get("output", "AGENTS.md"),
)
config_table.add_row(
"",
"Chatmode",
compilation_config.get("chatmode", "auto-detect"),
)
config_table.add_row(
"",
"Resolve Links",
str(compilation_config.get("resolve_links", True)),
)
else:
config_table.add_row(
"Compilation", "Status", "Using defaults (no config)"
)
else:
config_table.add_row(
"Project", "Status", "Not in an APM project directory"
)

config_table.add_row("Global", "APM CLI Version", get_version())

console.print(config_table)

except (ImportError, NameError):
# Fallback display
_rich_info("Current APM Configuration:")

if Path("apm.yml").exists():
apm_config = _load_apm_config()
click.echo(f"\n{HIGHLIGHT}Project (apm.yml):{RESET}")
click.echo(f" Name: {apm_config.get('name', 'Unknown')}")
click.echo(f" Version: {apm_config.get('version', 'Unknown')}")
click.echo(f" Entrypoint: {apm_config.get('entrypoint', 'None')}")
click.echo(
f" MCP Dependencies: {len(apm_config.get('dependencies', {}).get('mcp', []))}"
)
else:
_rich_info("Not in an APM project directory")

click.echo(f"\n{HIGHLIGHT}Global:{RESET}")
click.echo(f" APM CLI Version: {get_version()}")


@config.command(help="Set a configuration value")
@click.argument("key")
@click.argument("value")
def set(key, value):
"""Set a configuration value.

Examples:
apm config set auto-integrate false
apm config set auto-integrate true
"""
from ..config import set_auto_integrate

if key == "auto-integrate":
if value.lower() in ["true", "1", "yes"]:
set_auto_integrate(True)
_rich_success("Auto-integration enabled")
elif value.lower() in ["false", "0", "no"]:
set_auto_integrate(False)
_rich_success("Auto-integration disabled")
else:
_rich_error(f"Invalid value '{value}'. Use 'true' or 'false'.")
sys.exit(1)
else:
_rich_error(f"Unknown configuration key: '{key}'")
_rich_info("Valid keys: auto-integrate")
_rich_info(
"This error may indicate a bug in command routing. Please report this issue."
)
sys.exit(1)


@config.command(help="Get a configuration value")
@click.argument("key", required=False)
def get(key):
"""Get a configuration value or show all configuration.

Examples:
apm config get auto-integrate
apm config get
"""
from ..config import get_config, get_auto_integrate

if key:
if key == "auto-integrate":
value = get_auto_integrate()
click.echo(f"auto-integrate: {value}")
else:
_rich_error(f"Unknown configuration key: '{key}'")
_rich_info("Valid keys: auto-integrate")
_rich_info(
"This error may indicate a bug in command routing. Please report this issue."
)
sys.exit(1)
else:
# Show all config
config_data = get_config()
_rich_info("APM Configuration:")
for k, v in config_data.items():
# Map internal keys to user-friendly names
if k == "auto_integrate":
click.echo(f" auto-integrate: {v}")
else:
click.echo(f" {k}: {v}")
192 changes: 192 additions & 0 deletions src/apm_cli/commands/init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
"""APM init command."""

import os
import sys
from pathlib import Path

import click

from ..utils.console import (
_create_files_table,
_rich_echo,
_rich_error,
_rich_info,
_rich_panel,
_rich_success,
_rich_warning,
)
from ._helpers import (
INFO,
RESET,
_create_minimal_apm_yml,
_get_console,
_get_default_config,
_lazy_confirm,
_rich_blank_line,
)


@click.command(help="Initialize a new APM project")
@click.argument("project_name", required=False)
@click.option(
"--yes", "-y", is_flag=True, help="Skip interactive prompts and use auto-detected defaults"
)
@click.pass_context
def init(ctx, project_name, yes):
"""Initialize a new APM project (like npm init).

Creates a minimal apm.yml with auto-detected metadata.
"""
try:
# Handle explicit current directory
if project_name == ".":
project_name = None

# Determine project directory and name
if project_name:
project_dir = Path(project_name)
project_dir.mkdir(exist_ok=True)
os.chdir(project_dir)
_rich_info(f"Created project directory: {project_name}", symbol="folder")
final_project_name = project_name
else:
project_dir = Path.cwd()
final_project_name = project_dir.name

# Check for existing apm.yml
apm_yml_exists = Path("apm.yml").exists()

# Handle existing apm.yml in brownfield projects
if apm_yml_exists:
_rich_warning("apm.yml already exists")

if not yes:
Confirm = _lazy_confirm()
if Confirm:
try:
confirm = Confirm.ask("Continue and overwrite?")
except Exception:
confirm = click.confirm("Continue and overwrite?")
else:
confirm = click.confirm("Continue and overwrite?")

if not confirm:
_rich_info("Initialization cancelled.")
return
else:
_rich_info("--yes specified, overwriting apm.yml...")

# Get project configuration (interactive mode or defaults)
if not yes:
config = _interactive_project_setup(final_project_name)
else:
# Use auto-detected defaults
config = _get_default_config(final_project_name)

_rich_success(f"Initializing APM project: {config['name']}", symbol="rocket")

# Create minimal apm.yml
_create_minimal_apm_yml(config)

_rich_success("APM project initialized successfully!", symbol="sparkles")

# Display created file info
try:
console = _get_console()
if console:
files_data = [
("✨", "apm.yml", "Project configuration"),
]
table = _create_files_table(files_data, title="Created Files")
console.print(table)
except (ImportError, NameError):
_rich_info("Created:")
_rich_echo(" ✨ apm.yml - Project configuration", style="muted")

_rich_blank_line()

# Next steps - actionable commands matching README workflow
next_steps = [
"Install a runtime: apm runtime setup copilot",
"Add APM dependencies: apm install <owner>/<repo>",
"Compile agent context: apm compile",
"Run your first workflow: apm run start",
]

try:
_rich_panel(
"\n".join(f"• {step}" for step in next_steps),
title="💡 Next Steps",
style="cyan",
)
except (ImportError, NameError):
_rich_info("Next steps:")
for step in next_steps:
click.echo(f" • {step}")

except Exception as e:
_rich_error(f"Error initializing project: {e}")
sys.exit(1)


def _interactive_project_setup(default_name):
"""Interactive setup for new APM projects with auto-detection."""
from ._helpers import _auto_detect_author, _auto_detect_description

# Get auto-detected defaults
auto_author = _auto_detect_author()
auto_description = _auto_detect_description(default_name)

try:
# Lazy import rich pieces
from rich.console import Console # type: ignore
from rich.panel import Panel # type: ignore
from rich.prompt import Confirm, Prompt # type: ignore

console = _get_console() or Console()
console.print("\n[info]Setting up your APM project...[/info]")
console.print("[muted]Press ^C at any time to quit.[/muted]\n")

name = Prompt.ask("Project name", default=default_name).strip()
version = Prompt.ask("Version", default="1.0.0").strip()
description = Prompt.ask("Description", default=auto_description).strip()
author = Prompt.ask("Author", default=auto_author).strip()

summary_content = f"""name: {name}
version: {version}
description: {description}
author: {author}"""
console.print(
Panel(summary_content, title="About to create", border_style="cyan")
)

if not Confirm.ask("\nIs this OK?", default=True):
console.print("[info]Aborted.[/info]")
sys.exit(0)

except (ImportError, NameError):
# Fallback to click prompts
_rich_info("Setting up your APM project...")
_rich_info("Press ^C at any time to quit.")

name = click.prompt("Project name", default=default_name).strip()
version = click.prompt("Version", default="1.0.0").strip()
description = click.prompt("Description", default=auto_description).strip()
author = click.prompt("Author", default=auto_author).strip()

click.echo(f"\n{INFO}About to create:{RESET}")
click.echo(f" name: {name}")
click.echo(f" version: {version}")
click.echo(f" description: {description}")
click.echo(f" author: {author}")

if not click.confirm("\nIs this OK?", default=True):
_rich_info("Aborted.")
sys.exit(0)

return {
"name": name,
"version": version,
"description": description,
"author": author,
}
Loading