From e2779ded0b9339a75fe68a031a0aaca8d0115b5d Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Mon, 18 Aug 2025 13:11:41 -0400 Subject: [PATCH 1/7] Increase inquirer page limit --- arm_cli/cli.py | 2 + arm_cli/config.py | 1 + arm_cli/config_cmd.py | 72 ++++++++++++++++++++++++++++++++++ arm_cli/container/container.py | 3 ++ arm_cli/projects/activate.py | 6 ++- arm_cli/projects/init.py | 1 + arm_cli/projects/remove.py | 6 ++- 7 files changed, 89 insertions(+), 2 deletions(-) create mode 100644 arm_cli/config_cmd.py diff --git a/arm_cli/cli.py b/arm_cli/cli.py index 04c1511..b9c2f1f 100644 --- a/arm_cli/cli.py +++ b/arm_cli/cli.py @@ -7,6 +7,7 @@ from arm_cli import __version__ from arm_cli.config import load_config +from arm_cli.config_cmd import config from arm_cli.container.container import container from arm_cli.projects.projects import projects from arm_cli.self.self import self @@ -24,6 +25,7 @@ def cli(ctx): # Add command groups +cli.add_command(config) cli.add_command(container) cli.add_command(projects) cli.add_command(self) diff --git a/arm_cli/config.py b/arm_cli/config.py index 8d08adc..181d98a 100644 --- a/arm_cli/config.py +++ b/arm_cli/config.py @@ -62,6 +62,7 @@ class Config(BaseModel): active_project: str = "" available_projects: List[AvailableProject] = [] + inquirer_page_size: int = 20 def get_config_dir() -> Path: diff --git a/arm_cli/config_cmd.py b/arm_cli/config_cmd.py new file mode 100644 index 0000000..0476877 --- /dev/null +++ b/arm_cli/config_cmd.py @@ -0,0 +1,72 @@ +import click +import inquirer + +from arm_cli.config import load_config, save_config + + +@click.group() +def config(): + """Manage CLI configuration settings""" + pass + + +@config.command("show") +@click.pass_context +def show_config(ctx): + """Show current configuration settings""" + config = ctx.obj["config"] + + print("Current CLI Configuration:") + print(f" Inquirer Page Size: {config.inquirer_page_size}") + print(f" Active Project: {config.active_project or 'None'}") + print(f" Available Projects: {len(config.available_projects)}") + + +@config.command("set-page-size") +@click.argument("size", type=int) +@click.pass_context +def set_page_size(ctx, size): + """Set the inquirer page size for interactive menus""" + if size < 1: + print("Error: Page size must be at least 1") + return + + config = ctx.obj["config"] + old_size = config.inquirer_page_size + config.inquirer_page_size = size + save_config(config) + + print(f"Inquirer page size updated: {old_size} → {size}") + + +@config.command("interactive") +@click.pass_context +def interactive_config(ctx): + """Configure settings interactively""" + config = ctx.obj["config"] + + questions = [ + inquirer.Text( + "page_size", + message="Enter inquirer page size (number of items shown in menus)", + default=str(config.inquirer_page_size), + validate=lambda _, x: x.isdigit() and int(x) > 0 or "Please enter a positive number", + ) + ] + + try: + answers = inquirer.prompt(questions) + if answers is None: + print("Configuration cancelled.") + return + + new_page_size = int(answers["page_size"]) + if new_page_size != config.inquirer_page_size: + config.inquirer_page_size = new_page_size + save_config(config) + print(f"Inquirer page size updated to: {new_page_size}") + else: + print("No changes made.") + + except KeyboardInterrupt: + print("\nConfiguration cancelled.") diff --git a/arm_cli/container/container.py b/arm_cli/container/container.py index eac6fde..2184475 100644 --- a/arm_cli/container/container.py +++ b/arm_cli/container/container.py @@ -49,6 +49,7 @@ def attach_container(ctx): message="Select a container to attach to", choices=[f"{container.name} ({container.id[:12]})" for container in containers], carousel=True, + page_size=config.inquirer_page_size, ) ] @@ -82,6 +83,7 @@ def restart_container(ctx): message="Select a container to restart", choices=[f"{container.name} ({container.id[:12]})" for container in containers], carousel=True, + page_size=config.inquirer_page_size, ) ] @@ -122,6 +124,7 @@ def stop_container(ctx): message="Select a container to stop", choices=[f"{container.name} ({container.id[:12]})" for container in containers], carousel=True, + page_size=config.inquirer_page_size, ) ] diff --git a/arm_cli/projects/activate.py b/arm_cli/projects/activate.py index 0787aa9..2f7195a 100644 --- a/arm_cli/projects/activate.py +++ b/arm_cli/projects/activate.py @@ -43,7 +43,11 @@ def _activate(ctx, project: Optional[str] = None): # Create the question questions = [ inquirer.List( - "project", message="Select a project to activate", choices=choices, carousel=True + "project", + message="Select a project to activate", + choices=choices, + carousel=True, + page_size=config.inquirer_page_size, ) ] diff --git a/arm_cli/projects/init.py b/arm_cli/projects/init.py index 9ddb1b9..cae3af1 100644 --- a/arm_cli/projects/init.py +++ b/arm_cli/projects/init.py @@ -84,6 +84,7 @@ def _init(ctx, project_path: str, name: Optional[str] = None): message=f"Select a configuration file from {project_dir.name} or use default", choices=choices, carousel=True, + page_size=config.inquirer_page_size, ) ] diff --git a/arm_cli/projects/remove.py b/arm_cli/projects/remove.py index d8d289f..08bf065 100644 --- a/arm_cli/projects/remove.py +++ b/arm_cli/projects/remove.py @@ -33,7 +33,11 @@ def _remove(ctx, project: Optional[str] = None): # Create the question questions = [ inquirer.List( - "project", message="Select a project to remove", choices=choices, carousel=True + "project", + message="Select a project to remove", + choices=choices, + carousel=True, + page_size=config.inquirer_page_size, ) ] From eafc9f4b4f01d79eaa173f09f233013c391c4c37 Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Mon, 18 Aug 2025 13:55:47 -0400 Subject: [PATCH 2/7] Move inquirer page setting into a settings file --- arm_cli/config.py | 1 - arm_cli/config_cmd.py | 24 ++++++----- arm_cli/container/container.py | 8 ++-- arm_cli/projects/activate.py | 3 +- arm_cli/projects/init.py | 3 +- arm_cli/projects/remove.py | 3 +- arm_cli/settings.py | 74 ++++++++++++++++++++++++++++++++++ 7 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 arm_cli/settings.py diff --git a/arm_cli/config.py b/arm_cli/config.py index 181d98a..8d08adc 100644 --- a/arm_cli/config.py +++ b/arm_cli/config.py @@ -62,7 +62,6 @@ class Config(BaseModel): active_project: str = "" available_projects: List[AvailableProject] = [] - inquirer_page_size: int = 20 def get_config_dir() -> Path: diff --git a/arm_cli/config_cmd.py b/arm_cli/config_cmd.py index 0476877..1c298af 100644 --- a/arm_cli/config_cmd.py +++ b/arm_cli/config_cmd.py @@ -1,7 +1,8 @@ import click import inquirer -from arm_cli.config import load_config, save_config +from arm_cli.config import load_config +from arm_cli.settings import get_setting, load_settings, save_settings, set_setting @click.group() @@ -15,9 +16,10 @@ def config(): def show_config(ctx): """Show current configuration settings""" config = ctx.obj["config"] + settings = load_settings() print("Current CLI Configuration:") - print(f" Inquirer Page Size: {config.inquirer_page_size}") + print(f" Inquirer Page Size: {settings.inquirer_page_size}") print(f" Active Project: {config.active_project or 'None'}") print(f" Available Projects: {len(config.available_projects)}") @@ -31,10 +33,10 @@ def set_page_size(ctx, size): print("Error: Page size must be at least 1") return - config = ctx.obj["config"] - old_size = config.inquirer_page_size - config.inquirer_page_size = size - save_config(config) + settings = load_settings() + old_size = settings.inquirer_page_size + settings.inquirer_page_size = size + save_settings(settings) print(f"Inquirer page size updated: {old_size} → {size}") @@ -43,13 +45,13 @@ def set_page_size(ctx, size): @click.pass_context def interactive_config(ctx): """Configure settings interactively""" - config = ctx.obj["config"] + settings = load_settings() questions = [ inquirer.Text( "page_size", message="Enter inquirer page size (number of items shown in menus)", - default=str(config.inquirer_page_size), + default=str(settings.inquirer_page_size), validate=lambda _, x: x.isdigit() and int(x) > 0 or "Please enter a positive number", ) ] @@ -61,9 +63,9 @@ def interactive_config(ctx): return new_page_size = int(answers["page_size"]) - if new_page_size != config.inquirer_page_size: - config.inquirer_page_size = new_page_size - save_config(config) + if new_page_size != settings.inquirer_page_size: + settings.inquirer_page_size = new_page_size + save_settings(settings) print(f"Inquirer page size updated to: {new_page_size}") else: print("No changes made.") diff --git a/arm_cli/container/container.py b/arm_cli/container/container.py index 2184475..3656a5d 100644 --- a/arm_cli/container/container.py +++ b/arm_cli/container/container.py @@ -4,6 +4,8 @@ import docker import inquirer +from arm_cli.settings import get_setting + @click.group() def container(): @@ -49,7 +51,7 @@ def attach_container(ctx): message="Select a container to attach to", choices=[f"{container.name} ({container.id[:12]})" for container in containers], carousel=True, - page_size=config.inquirer_page_size, + page_size=get_setting("inquirer_page_size"), ) ] @@ -83,7 +85,7 @@ def restart_container(ctx): message="Select a container to restart", choices=[f"{container.name} ({container.id[:12]})" for container in containers], carousel=True, - page_size=config.inquirer_page_size, + page_size=get_setting("inquirer_page_size"), ) ] @@ -124,7 +126,7 @@ def stop_container(ctx): message="Select a container to stop", choices=[f"{container.name} ({container.id[:12]})" for container in containers], carousel=True, - page_size=config.inquirer_page_size, + page_size=get_setting("inquirer_page_size"), ) ] diff --git a/arm_cli/projects/activate.py b/arm_cli/projects/activate.py index 2f7195a..f6efa42 100644 --- a/arm_cli/projects/activate.py +++ b/arm_cli/projects/activate.py @@ -10,6 +10,7 @@ print_available_projects, print_no_projects_message, ) +from arm_cli.settings import get_setting def _activate(ctx, project: Optional[str] = None): @@ -47,7 +48,7 @@ def _activate(ctx, project: Optional[str] = None): message="Select a project to activate", choices=choices, carousel=True, - page_size=config.inquirer_page_size, + page_size=get_setting("inquirer_page_size"), ) ] diff --git a/arm_cli/projects/init.py b/arm_cli/projects/init.py index cae3af1..a2db234 100644 --- a/arm_cli/projects/init.py +++ b/arm_cli/projects/init.py @@ -12,6 +12,7 @@ load_project_config, save_config, ) +from arm_cli.settings import get_setting def _init(ctx, project_path: str, name: Optional[str] = None): @@ -84,7 +85,7 @@ def _init(ctx, project_path: str, name: Optional[str] = None): message=f"Select a configuration file from {project_dir.name} or use default", choices=choices, carousel=True, - page_size=config.inquirer_page_size, + page_size=get_setting("inquirer_page_size"), ) ] diff --git a/arm_cli/projects/remove.py b/arm_cli/projects/remove.py index 08bf065..73660f8 100644 --- a/arm_cli/projects/remove.py +++ b/arm_cli/projects/remove.py @@ -10,6 +10,7 @@ remove_project_from_list, save_config, ) +from arm_cli.settings import get_setting def _remove(ctx, project: Optional[str] = None): @@ -37,7 +38,7 @@ def _remove(ctx, project: Optional[str] = None): message="Select a project to remove", choices=choices, carousel=True, - page_size=config.inquirer_page_size, + page_size=get_setting("inquirer_page_size"), ) ] diff --git a/arm_cli/settings.py b/arm_cli/settings.py new file mode 100644 index 0000000..271699a --- /dev/null +++ b/arm_cli/settings.py @@ -0,0 +1,74 @@ +import json +import os +from pathlib import Path +from typing import Optional, Union + +import appdirs +from pydantic import BaseModel + + +class Settings(BaseModel): + """Settings schema for the CLI.""" + + inquirer_page_size: int = 20 + + +def get_settings_dir() -> Path: + """Get the settings directory for the CLI.""" + settings_dir = Path(appdirs.user_config_dir("arm-cli")) + settings_dir.mkdir(parents=True, exist_ok=True) + return settings_dir + + +def get_settings_file() -> Path: + """Get the path to the settings file.""" + return get_settings_dir() / "settings.json" + + +def load_settings() -> Settings: + """Load settings from file, creating default if it doesn't exist.""" + settings_file = get_settings_file() + + if not settings_file.exists(): + # Create default settings + default_settings = Settings() + save_settings(default_settings) + return default_settings + + try: + with open(settings_file, "r") as f: + data = json.load(f) + return Settings(**data) + except (json.JSONDecodeError, KeyError, TypeError) as e: + # If settings file is corrupted, create a new one + print(f"Warning: Settings file corrupted, creating new default settings: {e}") + default_settings = Settings() + save_settings(default_settings) + return default_settings + + +def save_settings(settings: Settings) -> None: + """Save settings to file.""" + settings_file = get_settings_file() + + # Ensure directory exists + settings_file.parent.mkdir(parents=True, exist_ok=True) + + with open(settings_file, "w") as f: + json.dump(settings.model_dump(), f, indent=2) + + +def get_setting(key: str) -> Optional[Union[int, str, bool]]: + """Get a specific setting value.""" + settings = load_settings() + return getattr(settings, key, None) + + +def set_setting(key: str, value: Union[int, str, bool]) -> None: + """Set a specific setting value.""" + settings = load_settings() + if hasattr(settings, key): + setattr(settings, key, value) + save_settings(settings) + else: + raise ValueError(f"Unknown setting: {key}") From f9bc027da2365256ec6704a5deb72344587b797e Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Mon, 18 Aug 2025 14:03:39 -0400 Subject: [PATCH 3/7] Move settings under self --- arm_cli/cli.py | 2 - arm_cli/config_cmd.py | 74 ------------ arm_cli/container/container.py | 6 +- arm_cli/projects/activate.py | 2 +- arm_cli/projects/init.py | 2 +- arm_cli/projects/remove.py | 2 +- arm_cli/self/self.py | 207 ++++++++++++++++++++++----------- arm_cli/settings.py | 2 +- 8 files changed, 145 insertions(+), 152 deletions(-) delete mode 100644 arm_cli/config_cmd.py diff --git a/arm_cli/cli.py b/arm_cli/cli.py index b9c2f1f..04c1511 100644 --- a/arm_cli/cli.py +++ b/arm_cli/cli.py @@ -7,7 +7,6 @@ from arm_cli import __version__ from arm_cli.config import load_config -from arm_cli.config_cmd import config from arm_cli.container.container import container from arm_cli.projects.projects import projects from arm_cli.self.self import self @@ -25,7 +24,6 @@ def cli(ctx): # Add command groups -cli.add_command(config) cli.add_command(container) cli.add_command(projects) cli.add_command(self) diff --git a/arm_cli/config_cmd.py b/arm_cli/config_cmd.py deleted file mode 100644 index 1c298af..0000000 --- a/arm_cli/config_cmd.py +++ /dev/null @@ -1,74 +0,0 @@ -import click -import inquirer - -from arm_cli.config import load_config -from arm_cli.settings import get_setting, load_settings, save_settings, set_setting - - -@click.group() -def config(): - """Manage CLI configuration settings""" - pass - - -@config.command("show") -@click.pass_context -def show_config(ctx): - """Show current configuration settings""" - config = ctx.obj["config"] - settings = load_settings() - - print("Current CLI Configuration:") - print(f" Inquirer Page Size: {settings.inquirer_page_size}") - print(f" Active Project: {config.active_project or 'None'}") - print(f" Available Projects: {len(config.available_projects)}") - - -@config.command("set-page-size") -@click.argument("size", type=int) -@click.pass_context -def set_page_size(ctx, size): - """Set the inquirer page size for interactive menus""" - if size < 1: - print("Error: Page size must be at least 1") - return - - settings = load_settings() - old_size = settings.inquirer_page_size - settings.inquirer_page_size = size - save_settings(settings) - - print(f"Inquirer page size updated: {old_size} → {size}") - - -@config.command("interactive") -@click.pass_context -def interactive_config(ctx): - """Configure settings interactively""" - settings = load_settings() - - questions = [ - inquirer.Text( - "page_size", - message="Enter inquirer page size (number of items shown in menus)", - default=str(settings.inquirer_page_size), - validate=lambda _, x: x.isdigit() and int(x) > 0 or "Please enter a positive number", - ) - ] - - try: - answers = inquirer.prompt(questions) - if answers is None: - print("Configuration cancelled.") - return - - new_page_size = int(answers["page_size"]) - if new_page_size != settings.inquirer_page_size: - settings.inquirer_page_size = new_page_size - save_settings(settings) - print(f"Inquirer page size updated to: {new_page_size}") - else: - print("No changes made.") - - except KeyboardInterrupt: - print("\nConfiguration cancelled.") diff --git a/arm_cli/container/container.py b/arm_cli/container/container.py index 3656a5d..396e8de 100644 --- a/arm_cli/container/container.py +++ b/arm_cli/container/container.py @@ -51,7 +51,7 @@ def attach_container(ctx): message="Select a container to attach to", choices=[f"{container.name} ({container.id[:12]})" for container in containers], carousel=True, - page_size=get_setting("inquirer_page_size"), + page_size=get_setting("menu_page_size"), ) ] @@ -85,7 +85,7 @@ def restart_container(ctx): message="Select a container to restart", choices=[f"{container.name} ({container.id[:12]})" for container in containers], carousel=True, - page_size=get_setting("inquirer_page_size"), + page_size=get_setting("menu_page_size"), ) ] @@ -126,7 +126,7 @@ def stop_container(ctx): message="Select a container to stop", choices=[f"{container.name} ({container.id[:12]})" for container in containers], carousel=True, - page_size=get_setting("inquirer_page_size"), + page_size=get_setting("menu_page_size"), ) ] diff --git a/arm_cli/projects/activate.py b/arm_cli/projects/activate.py index f6efa42..649777d 100644 --- a/arm_cli/projects/activate.py +++ b/arm_cli/projects/activate.py @@ -48,7 +48,7 @@ def _activate(ctx, project: Optional[str] = None): message="Select a project to activate", choices=choices, carousel=True, - page_size=get_setting("inquirer_page_size"), + page_size=get_setting("menu_page_size"), ) ] diff --git a/arm_cli/projects/init.py b/arm_cli/projects/init.py index a2db234..dac3156 100644 --- a/arm_cli/projects/init.py +++ b/arm_cli/projects/init.py @@ -85,7 +85,7 @@ def _init(ctx, project_path: str, name: Optional[str] = None): message=f"Select a configuration file from {project_dir.name} or use default", choices=choices, carousel=True, - page_size=get_setting("inquirer_page_size"), + page_size=get_setting("menu_page_size"), ) ] diff --git a/arm_cli/projects/remove.py b/arm_cli/projects/remove.py index 73660f8..6d4b91f 100644 --- a/arm_cli/projects/remove.py +++ b/arm_cli/projects/remove.py @@ -38,7 +38,7 @@ def _remove(ctx, project: Optional[str] = None): message="Select a project to remove", choices=choices, carousel=True, - page_size=get_setting("inquirer_page_size"), + page_size=get_setting("menu_page_size"), ) ] diff --git a/arm_cli/self/self.py b/arm_cli/self/self.py index 44790af..c72e910 100644 --- a/arm_cli/self/self.py +++ b/arm_cli/self/self.py @@ -4,6 +4,7 @@ from pathlib import Path import click +import inquirer from click.core import ParameterSource from arm_cli.config import ( @@ -12,6 +13,7 @@ load_project_config, save_config, ) +from arm_cli.settings import get_setting, load_settings, save_settings, set_setting @click.group() @@ -65,79 +67,146 @@ def update(ctx, source, force): print("arm-cli updated successfully!") -@self.command() -@click.option("--project", help="Set the active project config file") -@click.option("--list-projects", is_flag=True, help="List all available project config files") -@click.option("--show-project", is_flag=True, help="Show current project configuration") +@self.group() +def settings(): + """Manage CLI settings""" + pass + + +@settings.command("show") @click.pass_context -def config(ctx, project, list_projects, show_project): - """Manage CLI configuration""" - config = ctx.obj["config"] - - if list_projects: - config_dir = get_config_dir() - print("Available project config files:") - for config_file in config_dir.glob("*.json"): - if config_file.name != "config.json": # Skip the main config file - active_marker = " (active)" if str(config_file) == config.active_project else "" - print(f" {config_file.name}{active_marker}") - - # Try to load and show description - try: - project_config = load_project_config(str(config_file)) - if project_config.description: - print(f" Description: {project_config.description}") - except Exception: - print(f" (Could not load config)") +def show_settings(ctx): + """Show the settings file path and contents""" + from arm_cli.settings import get_settings_file + + settings_file = get_settings_file() + print(f"Settings file: {settings_file}") + print() + + if settings_file.exists(): + with open(settings_file, "r") as f: + print(f.read()) + else: + print("Settings file does not exist yet.") + + +@settings.command("set") +@click.argument("key", required=False) +@click.argument("value", required=False) +@click.pass_context +def set_settings(ctx, key, value): + """Set a configuration value""" + from arm_cli.settings import get_setting, load_settings, save_settings, set_setting + + settings = load_settings() + + # If no arguments provided, launch interactive mode + if key is None and value is None: + # Get all available settings + available_settings = list(settings.model_fields.keys()) + + # Create choices for inquirer + choices = [] + for setting_key in available_settings: + current_val = getattr(settings, setting_key) + choices.append(f"{setting_key} (current: {current_val})") + + # Ask user to select a setting + questions = [ + inquirer.List( + "setting", + message="Select a setting to modify", + choices=choices, + carousel=True, + ) + ] + + try: + answers = inquirer.prompt(questions) + if answers is None: + print("Configuration cancelled.") + return + + # Extract the key from the selected choice + selected_choice = answers["setting"] + key = selected_choice.split(" (current:")[0] + + except KeyboardInterrupt: + print("\nConfiguration cancelled.") + return + + # If only key is provided, ask for value + if key is not None and value is None: + current_value = getattr(settings, key) + + # Create appropriate input question based on type + if isinstance(current_value, bool): + questions = [ + inquirer.List( + "value", + message=f"Set {key} (current: {current_value})", + choices=["true", "false"], + default="true" if current_value else "false", + carousel=True, + ) + ] + else: + questions = [ + inquirer.Text( + "value", + message=f"Set {key} (current: {current_value})", + default=str(current_value), + ) + ] + + try: + answers = inquirer.prompt(questions) + if answers is None: + print("Configuration cancelled.") + return + + value = answers["value"] + + except KeyboardInterrupt: + print("\nConfiguration cancelled.") + return + + # Now we have both key and value, proceed with validation and setting + if key is None or value is None: + print("Error: Both key and value are required.") + return + + # Check if the key exists in settings + if not hasattr(settings, key): + print(f"Error: Unknown setting '{key}'") + print(f"Available settings: {', '.join(settings.model_fields.keys())}") return - if show_project: - project_config = get_active_project_config(config) - if project_config: - print(f"Active project: {project_config.name}") - print(f"Config file: {config.active_project}") - print(f"Description: {project_config.description or 'No description'}") - print(f"Docker compose file: {project_config.docker_compose_file or 'Not specified'}") - print(f"Data directory: {project_config.data_directory or 'Not specified'}") - - if project_config.resources: - print("Resources:") - for resource_name, resource_path in project_config.resources.items(): - print(f" {resource_name}: {resource_path}") - - if project_config.skills: - print("Skills:") - for skill_name, skill_path in project_config.skills.items(): - print(f" {skill_name}: {skill_path}") - - if project_config.monitoring: - print("Monitoring:") - for service_name, service_url in project_config.monitoring.items(): - print(f" {service_name}: {service_url}") + # Get the current value + current_value = getattr(settings, key) + + # Convert value to the appropriate type based on the current value + try: + if isinstance(current_value, int): + new_value = int(value) + elif isinstance(current_value, bool): + if value.lower() in ("true", "1", "yes", "on"): + new_value = True + elif value.lower() in ("false", "0", "no", "off"): + new_value = False + else: + print( + f"Error: Invalid boolean value '{value}'. Use true/false, 1/0, yes/no, or on/off" + ) + return else: - print("No active project configuration found.") + new_value = value + except ValueError as e: + print(f"Error: Invalid value '{value}' for setting '{key}': {e}") return - if project is not None: - # Check if the project file exists - project_path = Path(project) - if not project_path.is_absolute(): - project_path = get_config_dir() / project_path - - if not project_path.exists(): - print(f"Error: Project config file '{project}' not found.") - print("Available project config files:") - config_dir = get_config_dir() - for config_file in config_dir.glob("*.json"): - if config_file.name != "config.json": - print(f" {config_file.name}") - return + # Set the new value + setattr(settings, key, new_value) + save_settings(settings) - config.active_project = str(project_path) - save_config(config) - print(f"Active project set to: {project}") - else: - print(f"Active project: {config.active_project or 'None (will use default)'}") - print("Use --project to set a new active project") - print("Use --list-projects to see all available project config files") - print("Use --show-project to see current project configuration") + print(f"Updated {key}: {current_value} → {new_value}") diff --git a/arm_cli/settings.py b/arm_cli/settings.py index 271699a..74cefe8 100644 --- a/arm_cli/settings.py +++ b/arm_cli/settings.py @@ -10,7 +10,7 @@ class Settings(BaseModel): """Settings schema for the CLI.""" - inquirer_page_size: int = 20 + menu_page_size: int = 20 def get_settings_dir() -> Path: From 9265574dd8e521282c267a8f28cbc1a9e70c0f56 Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Mon, 18 Aug 2025 14:19:39 -0400 Subject: [PATCH 4/7] Rename Config to GlobalContext --- arm_cli/config.py | 49 ++++++++++++++++++++++++++++++++------------- arm_cli/settings.py | 1 + 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/arm_cli/config.py b/arm_cli/config.py index 8d08adc..4aeb483 100644 --- a/arm_cli/config.py +++ b/arm_cli/config.py @@ -57,8 +57,8 @@ class AvailableProject(BaseModel): path: str -class Config(BaseModel): - """Configuration schema for the CLI.""" +class GlobalContext(BaseModel): + """Global context schema for the CLI.""" active_project: str = "" available_projects: List[AvailableProject] = [] @@ -73,7 +73,12 @@ def get_config_dir() -> Path: def get_config_file() -> Path: """Get the path to the configuration file.""" - return get_config_dir() / "config.json" + from arm_cli.settings import get_setting + + # Get the filename from settings, default to "global_context.json" + filename_setting = get_setting("global_context_path") + filename = filename_setting if isinstance(filename_setting, str) else "global_context.json" + return get_config_dir() / filename def get_default_project_config_path() -> Path: @@ -141,7 +146,7 @@ def load_project_config(project_path: str) -> ProjectConfig: return project_config -def add_project_to_list(config: Config, project_path: str, project_name: str) -> None: +def add_project_to_list(config: GlobalContext, project_path: str, project_name: str) -> None: """Add a project to the available projects list and set as active.""" # Remove existing entry if it exists config.available_projects = [p for p in config.available_projects if p.path != project_path] @@ -154,7 +159,7 @@ def add_project_to_list(config: Config, project_path: str, project_name: str) -> config.active_project = project_path -def get_available_projects(config: Config) -> List[AvailableProject]: +def get_available_projects(config: GlobalContext) -> List[AvailableProject]: """Get the list of available projects.""" # If no projects are available, ensure the default project is added if not config.available_projects: @@ -168,7 +173,7 @@ def get_available_projects(config: Config) -> List[AvailableProject]: return config.available_projects -def activate_project(config: Config, project_identifier: str) -> Optional[ProjectConfig]: +def activate_project(config: GlobalContext, project_identifier: str) -> Optional[ProjectConfig]: """Activate a project by path or name.""" # First try to find by exact path for project in config.available_projects: @@ -187,7 +192,7 @@ def activate_project(config: Config, project_identifier: str) -> Optional[Projec return None -def remove_project_from_list(config: Config, project_identifier: str) -> bool: +def remove_project_from_list(config: GlobalContext, project_identifier: str) -> bool: """Remove a project from the available projects list by path or name.""" # First try to find by exact path for project in config.available_projects: @@ -210,29 +215,45 @@ def remove_project_from_list(config: Config, project_identifier: str) -> bool: return False -def load_config() -> Config: +def load_config() -> GlobalContext: """Load configuration from file, creating default if it doesn't exist.""" config_file = get_config_file() + # Check for old config file and migrate if needed + old_config_file = get_config_dir() / "config.json" + if old_config_file.exists() and not config_file.exists(): + print(f"Migrating from old config file: {old_config_file}") + try: + with open(old_config_file, "r") as f: + data = json.load(f) + config = GlobalContext(**data) + save_config(config) + # Remove old file after successful migration + old_config_file.unlink() + print(f"Migration complete. New file: {config_file}") + return config + except Exception as e: + print(f"Migration failed: {e}. Creating new config file.") + if not config_file.exists(): # Create default config - default_config = Config() + default_config = GlobalContext() save_config(default_config) return default_config try: with open(config_file, "r") as f: data = json.load(f) - return Config(**data) + return GlobalContext(**data) except (json.JSONDecodeError, KeyError, TypeError) as e: # If config is corrupted, create a new one print(f"Warning: Config file corrupted, creating new default config: {e}") - default_config = Config() + default_config = GlobalContext() save_config(default_config) return default_config -def save_config(config: Config) -> None: +def save_config(config: GlobalContext) -> None: """Save configuration to file.""" config_file = get_config_file() @@ -243,7 +264,7 @@ def save_config(config: Config) -> None: json.dump(config.model_dump(), f, indent=2) -def get_active_project_config(config: Config) -> Optional[ProjectConfig]: +def get_active_project_config(config: GlobalContext) -> Optional[ProjectConfig]: """Get the active project configuration.""" if not config.active_project: # No active project set, copy default and set it @@ -275,7 +296,7 @@ def print_no_projects_message() -> None: print("No projects available. Use 'arm-cli projects init ' to add a project.") -def print_available_projects(config: Config) -> None: +def print_available_projects(config: GlobalContext) -> None: """Print the list of available projects.""" available_projects = get_available_projects(config) if available_projects: diff --git a/arm_cli/settings.py b/arm_cli/settings.py index 74cefe8..d16e56e 100644 --- a/arm_cli/settings.py +++ b/arm_cli/settings.py @@ -11,6 +11,7 @@ class Settings(BaseModel): """Settings schema for the CLI.""" menu_page_size: int = 20 + global_context_path: str = "global_context.json" def get_settings_dir() -> Path: From 4bb25e7e43428b28ecca225d479a98d48ab6e2d1 Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Mon, 18 Aug 2025 14:30:42 -0400 Subject: [PATCH 5/7] Add a getter for settings --- arm_cli/self/self.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/arm_cli/self/self.py b/arm_cli/self/self.py index c72e910..4f7d48e 100644 --- a/arm_cli/self/self.py +++ b/arm_cli/self/self.py @@ -90,6 +90,26 @@ def show_settings(ctx): print("Settings file does not exist yet.") +@settings.command("get") +@click.argument("key") +@click.pass_context +def get_settings_cmd(ctx, key): + """Get a specific setting value""" + from arm_cli.settings import get_setting + + value = get_setting(key) + if value is not None: + print(value) + else: + print(f"Error: Unknown setting '{key}'") + # Get available settings to show user + from arm_cli.settings import load_settings + + settings = load_settings() + print(f"Available settings: {', '.join(settings.model_fields.keys())}") + sys.exit(1) + + @settings.command("set") @click.argument("key", required=False) @click.argument("value", required=False) From ee23642e5ed3be385832380113389284058e63b8 Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Mon, 18 Aug 2025 14:35:32 -0400 Subject: [PATCH 6/7] Add cdc alias for cd to code --- arm_cli/settings.py | 1 + arm_cli/system/shell_scripts/shell_addins.sh | 3 +++ 2 files changed, 4 insertions(+) diff --git a/arm_cli/settings.py b/arm_cli/settings.py index d16e56e..7199a6e 100644 --- a/arm_cli/settings.py +++ b/arm_cli/settings.py @@ -12,6 +12,7 @@ class Settings(BaseModel): menu_page_size: int = 20 global_context_path: str = "global_context.json" + cdc_path: str = "~/code" def get_settings_dir() -> Path: diff --git a/arm_cli/system/shell_scripts/shell_addins.sh b/arm_cli/system/shell_scripts/shell_addins.sh index da032cc..26f763a 100644 --- a/arm_cli/system/shell_scripts/shell_addins.sh +++ b/arm_cli/system/shell_scripts/shell_addins.sh @@ -39,6 +39,9 @@ setup_alias() { # Add cdp alias to change to project directory alias cdp='cd "$(arm-cli projects info --field "project_directory" | sed "s|^~|$HOME|")"' + + # Add cdc alias to change to code directory + alias cdc='cd "$(arm-cli self settings get cdc_path | sed "s|^~|$HOME|")"' fi fi } From 43c66a632ace1175a7e99a4fde2fa2e6f833cec4 Mon Sep 17 00:00:00 2001 From: Matthew Powelson Date: Mon, 18 Aug 2025 14:40:51 -0400 Subject: [PATCH 7/7] Update unit tests --- tests/test_config.py | 26 +++++++++++++------------- tests/unit/projects/test_projects.py | 10 ++++++---- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/test_config.py b/tests/test_config.py index 033b00d..7e752ff 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -6,7 +6,7 @@ import pytest from arm_cli.config import ( - Config, + GlobalContext, ProjectConfig, get_config_dir, get_config_file, @@ -16,20 +16,20 @@ ) -class TestConfig: - def test_config_default_values(self): - """Test that Config has correct default values.""" - config = Config() +class TestGlobalContext: + def test_global_context_default_values(self): + """Test that GlobalContext has correct default values.""" + config = GlobalContext() assert config.active_project == "" - def test_config_with_values(self): - """Test that Config can be created with custom values.""" - config = Config(active_project="test-project") + def test_global_context_with_values(self): + """Test that GlobalContext can be created with custom values.""" + config = GlobalContext(active_project="test-project") assert config.active_project == "test-project" - def test_config_model_dump(self): - """Test that Config can be serialized to dict.""" - config = Config(active_project="test-project") + def test_global_context_model_dump(self): + """Test that GlobalContext can be serialized to dict.""" + config = GlobalContext(active_project="test-project") data = config.model_dump() assert data == {"active_project": "test-project", "available_projects": []} @@ -52,7 +52,7 @@ def test_get_config_file(self): config_file = get_config_file() - assert config_file == Path("/tmp/test_config/config.json") + assert config_file == Path("/tmp/test_config/global_context.json") def test_save_config(self): """Test that config can be saved to file.""" @@ -61,7 +61,7 @@ def test_save_config(self): config_file = Path(temp_dir) / "config.json" mock_get_config_file.return_value = config_file - config = Config(active_project="test-project") + config = GlobalContext(active_project="test-project") save_config(config) assert config_file.exists() diff --git a/tests/unit/projects/test_projects.py b/tests/unit/projects/test_projects.py index 6c2d6ac..ba438ad 100644 --- a/tests/unit/projects/test_projects.py +++ b/tests/unit/projects/test_projects.py @@ -7,7 +7,7 @@ import pytest from click.testing import CliRunner -from arm_cli.config import Config, ProjectConfig +from arm_cli.config import GlobalContext, ProjectConfig from arm_cli.projects.projects import projects @@ -33,7 +33,7 @@ def temp_project_config(): @pytest.fixture def mock_config(temp_project_config): """Create a mock config with an active project.""" - config = Config(active_project="/tmp/test-project-config.json") + config = GlobalContext(active_project="/tmp/test-project-config.json") return config @@ -42,7 +42,7 @@ def test_projects_info_no_active_project(runner): with patch("arm_cli.projects.info.get_active_project_config") as mock_get_config: mock_get_config.return_value = None - result = runner.invoke(projects, ["info"], obj={"config": Config()}) + result = runner.invoke(projects, ["info"], obj={"config": GlobalContext()}) assert result.exit_code == 0 assert "No active project configured" in result.output @@ -191,7 +191,9 @@ def test_projects_info_field_no_active_project(runner): with patch("arm_cli.projects.info.get_active_project_config") as mock_get_config: mock_get_config.return_value = None - result = runner.invoke(projects, ["info", "--field", "name"], obj={"config": Config()}) + result = runner.invoke( + projects, ["info", "--field", "name"], obj={"config": GlobalContext()} + ) assert result.exit_code == 0 assert "No active project configured" in result.output