Skip to content

Commit

Permalink
Migrate config to XDG Base Directory standard
Browse files Browse the repository at this point in the history
- Add XDG config path support (/abc/config)
- Add migration path for existing ~/.abc.conf configs
- Add clear messaging to encourage removal of legacy config:
  * Warning in abc_generate when both configs exist
  * Deprecation notice in legacy config after migration
- Update documentation with new config paths
- Maintain backward compatibility with legacy location

fixes #20
  • Loading branch information
ehammond committed Dec 30, 2024
1 parent b9e6de7 commit 71242a7
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 20 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ Remember to always review the generated commands before executing them, especial
The program will attempt to read the config file from the first of these values provided:
1. `--config` command line option
2. $ABC_CONFIG environment variable
3. $HOME/.abc.conf
3. $XDG_CONFIG_HOME/abc/config (defaults to ~/.config/abc/config)
4. ~/.abc.conf (legacy, will be removed in a future version)

Configuration sections allow using different LLM providers and models:

Expand Down
20 changes: 17 additions & 3 deletions abc_cli/abc_generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
PROGRAM_NAME: str = "abc"

# Config file
DEFAULT_CONFIG_FILE: str = '~/.abc.conf'
DEFAULT_CONFIG_SECTION: str = 'default'
DEFAULT_PROVIDER: str = 'anthropic'

Expand All @@ -60,7 +59,6 @@ def create_argument_parser() -> argparse.ArgumentParser:
"""Create an argument parser for command-line arguments."""
parser = argparse.ArgumentParser(prog=PROGRAM_NAME, description="abc - AI Bash Command Generator")
parser.add_argument('-c', '--config', type=argparse.FileType('r'),
default=os.environ.get('ABC_CONFIG', os.path.expanduser(DEFAULT_CONFIG_FILE)),
help='Path to configuration file')
parser.add_argument('--version', action='version', version=VERSION,
help='Display the program version and exit')
Expand All @@ -82,6 +80,22 @@ def create_argument_parser() -> argparse.ArgumentParser:
help='English description of the desired shell command')
return parser

def get_config_file() -> str:
"""Get config file path with XDG support."""
# Check CLI override or environment variable first
if config_override := os.environ.get('ABC_CONFIG'):
return os.path.expanduser(config_override)

# XDG config path
xdg_config_home = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
xdg_config = os.path.join(xdg_config_home, 'abc', 'config')

# Legacy config path
legacy_config = os.path.expanduser('~/.abc.conf')

# Return first existing config or default to XDG path
return xdg_config if os.path.exists(xdg_config) else legacy_config

def get_config(config_file_path: str, section: str = DEFAULT_CONFIG_SECTION) -> Dict[str, str]:
"""Read and parse the configuration file, using the specified section."""
config = configparser.ConfigParser()
Expand Down Expand Up @@ -156,7 +170,7 @@ def main() -> int:

setup_logging(args.log_level)

config_file_path = args.config.name if args.config else os.environ.get('ABC_CONFIG', os.path.expanduser(DEFAULT_CONFIG_FILE))
config_file_path = args.config.name if args.config else get_config_file()
section = args.use if args.use else DEFAULT_CONFIG_SECTION
config = get_config(config_file_path, section)

Expand Down
75 changes: 59 additions & 16 deletions abc_cli/abc_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,57 @@ def try_modify_rc_file(file_path, source_line, remove=False):
write_rc_file(file_path, new_lines)
return True

def get_config_paths():
"""Get config file paths following XDG Base Directory Specification."""
# Legacy config path
legacy_config = Path.home() / '.abc.conf'

# XDG config path
xdg_config_home = os.environ.get('XDG_CONFIG_HOME', Path.home() / '.config')
xdg_config = Path(xdg_config_home) / 'abc' / 'config'

return xdg_config, legacy_config

def setup_config(no_prompt=False):
"""Set up abc configuration file with API key."""
config_file = Path.home() / '.abc.conf'
xdg_config, legacy_config = get_config_paths()

# Check for existing configs
if legacy_config.exists():
if not prompt_user("\nFound config at ~/.abc.conf. Would you like to migrate it to the XDG location?", default=True, no_prompt=no_prompt):
return True

# Create XDG config directory
xdg_config.parent.mkdir(parents=True, exist_ok=True)

# Copy existing config to XDG location
shutil.copy2(legacy_config, xdg_config)
xdg_config.chmod(0o600)

# Add deprecation warning to old config
with open(legacy_config, 'r') as f:
old_content = f.read()
with open(legacy_config, 'w') as f:
f.write("# This configuration file is deprecated.\n")
f.write(f"# Please use {xdg_config} instead.\n")
f.write("# This file will be removed in a future version.\n\n")
f.write(old_content)

logging.info(f"Migrated config to: {xdg_config}")
return True

# Check if we should configure
if config_file.exists() and not prompt_user("\nConfiguration file already exists. Would you like to reconfigure it?", default=False, no_prompt=no_prompt):
# Check if XDG config exists
if xdg_config.exists() and not prompt_user("\nConfiguration file already exists. Would you like to reconfigure it?", default=False, no_prompt=no_prompt):
return True

try:
# Backup existing config if it exists
if config_file.exists():
# Create XDG config directory
xdg_config.parent.mkdir(parents=True, exist_ok=True)

# Backup existing XDG config if it exists
if xdg_config.exists():
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_file(config_file, timestamp)
backup_file(xdg_config, timestamp)

# Get the template content
with importlib.resources.path('abc_cli', 'abc.conf.template') as template_path:
Expand All @@ -256,12 +294,12 @@ def setup_config(no_prompt=False):

# Write configuration
config_content = template_content.replace('{ANTHROPIC_API_KEY}', api_key)
with open(config_file, 'w') as f:
with open(xdg_config, 'w') as f:
f.write(config_content)

# Set restrictive permissions on config file since it contains sensitive data
config_file.chmod(0o600)
logging.info(f"Created configuration file with restricted permissions (600): {config_file}")
xdg_config.chmod(0o600)
logging.info(f"Created configuration file with restricted permissions (600): {xdg_config}")
return True

except (OSError, IOError) as e:
Expand Down Expand Up @@ -369,13 +407,18 @@ def uninstall(no_prompt=False):
if try_modify_rc_file(rc_file, '', remove=True):
modified = True

# Optionally remove configuration
config_file = Path.home() / '.abc.conf'
if config_file.exists() and prompt_user("\nWould you like to remove the configuration file (~/.abc.conf)?", default=False, no_prompt=no_prompt):
# Create backup before removal since we know we'll modify it
backup_file(config_file, timestamp)
config_file.unlink()
logging.info("Removed configuration file")
# Optionally remove configuration files
xdg_config, legacy_config = get_config_paths()

if xdg_config.exists() and prompt_user("\nWould you like to remove the configuration file?", default=False, no_prompt=no_prompt):
backup_file(xdg_config, timestamp)
xdg_config.unlink()
logging.info(f"Removed configuration file: {xdg_config}")

if legacy_config.exists() and prompt_user("\nWould you like to remove the legacy configuration file (~/.abc.conf)?", default=False, no_prompt=no_prompt):
backup_file(legacy_config, timestamp)
legacy_config.unlink()
logging.info("Removed legacy configuration file")

print("\nUninstallation complete. You may now run:")
print("pipx uninstall abc-cli")
Expand Down

0 comments on commit 71242a7

Please sign in to comment.