@@ -0,0 +1,227 @@ |
| | 1 | +#!/usr/bin/env python3 |
| | 2 | +""" |
| | 3 | +shtick - Shell alias manager |
| | 4 | +Generates shell configuration files from TOML config |
| | 5 | +""" |
| | 6 | + |
| | 7 | +import sys |
| | 8 | +import argparse |
| | 9 | +import os |
| | 10 | +from typing import Optional |
| | 11 | + |
| | 12 | +# Package imports |
| | 13 | +from shtick.config import Config |
| | 14 | +from shtick.generator import Generator |
| | 15 | +from shtick.shells import get_supported_shells |
| | 16 | + |
| | 17 | + |
| | 18 | +def cmd_generate(args) -> None: |
| | 19 | + """Generate shell files from config""" |
| | 20 | + config_path = args.config or Config.get_default_config_path() |
| | 21 | + |
| | 22 | + try: |
| | 23 | + config = Config(config_path) |
| | 24 | + config.load() |
| | 25 | + |
| | 26 | + generator = Generator() |
| | 27 | + generator.generate_all(config) |
| | 28 | + |
| | 29 | + except FileNotFoundError as e: |
| | 30 | + print(f"Error: {e}") |
| | 31 | + print(f"Create a config file at {config_path} first") |
| | 32 | + sys.exit(1) |
| | 33 | + except Exception as e: |
| | 34 | + print(f"Error: {e}") |
| | 35 | + sys.exit(1) |
| | 36 | + |
| | 37 | + |
| | 38 | +def cmd_add(args) -> None: |
| | 39 | + """Add an item to the config""" |
| | 40 | + if "=" not in args.assignment: |
| | 41 | + print("Error: Assignment must be in format key=value") |
| | 42 | + sys.exit(1) |
| | 43 | + |
| | 44 | + key, value = args.assignment.split("=", 1) |
| | 45 | + key = key.strip() |
| | 46 | + value = value.strip() |
| | 47 | + |
| | 48 | + if not key or not value: |
| | 49 | + print("Error: Both key and value must be non-empty") |
| | 50 | + sys.exit(1) |
| | 51 | + |
| | 52 | + config_path = Config.get_default_config_path() |
| | 53 | + |
| | 54 | + try: |
| | 55 | + config = Config(config_path) |
| | 56 | + # Try to load existing config, create empty if doesn't exist |
| | 57 | + try: |
| | 58 | + config.load() |
| | 59 | + except FileNotFoundError: |
| | 60 | + print(f"Creating new config file at {config_path}") |
| | 61 | + |
| | 62 | + config.add_item(args.type, args.group, key, value) |
| | 63 | + config.save() |
| | 64 | + |
| | 65 | + print(f"Added {args.type} '{key}' = '{value}' to group '{args.group}'") |
| | 66 | + |
| | 67 | + except Exception as e: |
| | 68 | + print(f"Error: {e}") |
| | 69 | + sys.exit(1) |
| | 70 | + |
| | 71 | + |
| | 72 | +def cmd_remove(args) -> None: |
| | 73 | + """Remove an item from the config""" |
| | 74 | + config_path = Config.get_default_config_path() |
| | 75 | + |
| | 76 | + try: |
| | 77 | + config = Config(config_path) |
| | 78 | + config.load() |
| | 79 | + |
| | 80 | + # Find matching items |
| | 81 | + matches = config.find_items(args.type, args.group, args.search) |
| | 82 | + |
| | 83 | + if not matches: |
| | 84 | + print( |
| | 85 | + f"No {args.type} items matching '{args.search}' found in group '{args.group}'" |
| | 86 | + ) |
| | 87 | + return |
| | 88 | + |
| | 89 | + if len(matches) == 1: |
| | 90 | + # Exact match, remove it |
| | 91 | + item = matches[0] |
| | 92 | + if config.remove_item(args.type, args.group, item): |
| | 93 | + config.save() |
| | 94 | + print(f"Removed {args.type} '{item}' from group '{args.group}'") |
| | 95 | + else: |
| | 96 | + print(f"Failed to remove {args.type} '{item}'") |
| | 97 | + else: |
| | 98 | + # Multiple matches, ask for confirmation |
| | 99 | + print(f"Found {len(matches)} matches:") |
| | 100 | + for i, item in enumerate(matches, 1): |
| | 101 | + print(f" {i}. {item}") |
| | 102 | + |
| | 103 | + try: |
| | 104 | + choice = input("Enter number to remove (or 'q' to quit): ").strip() |
| | 105 | + if choice.lower() == "q": |
| | 106 | + print("Cancelled") |
| | 107 | + return |
| | 108 | + |
| | 109 | + idx = int(choice) - 1 |
| | 110 | + if 0 <= idx < len(matches): |
| | 111 | + item = matches[idx] |
| | 112 | + if config.remove_item(args.type, args.group, item): |
| | 113 | + config.save() |
| | 114 | + print(f"Removed {args.type} '{item}' from group '{args.group}'") |
| | 115 | + else: |
| | 116 | + print(f"Failed to remove {args.type} '{item}'") |
| | 117 | + else: |
| | 118 | + print("Invalid choice") |
| | 119 | + except (ValueError, KeyboardInterrupt): |
| | 120 | + print("\nCancelled") |
| | 121 | + |
| | 122 | + except FileNotFoundError: |
| | 123 | + print(f"Config file not found: {config_path}") |
| | 124 | + sys.exit(1) |
| | 125 | + except Exception as e: |
| | 126 | + print(f"Error: {e}") |
| | 127 | + sys.exit(1) |
| | 128 | + |
| | 129 | + |
| | 130 | +def cmd_list(args) -> None: |
| | 131 | + """List current configuration""" |
| | 132 | + config_path = Config.get_default_config_path() |
| | 133 | + |
| | 134 | + try: |
| | 135 | + config = Config(config_path) |
| | 136 | + config.load() |
| | 137 | + |
| | 138 | + if not config.groups: |
| | 139 | + print("No groups configured") |
| | 140 | + return |
| | 141 | + |
| | 142 | + for group in config.groups: |
| | 143 | + print(f"\nGroup: {group.name}") |
| | 144 | + |
| | 145 | + if group.aliases: |
| | 146 | + print(f" Aliases ({len(group.aliases)}):") |
| | 147 | + for key, value in group.aliases.items(): |
| | 148 | + print(f" {key} = {value}") |
| | 149 | + |
| | 150 | + if group.env_vars: |
| | 151 | + print(f" Environment Variables ({len(group.env_vars)}):") |
| | 152 | + for key, value in group.env_vars.items(): |
| | 153 | + print(f" {key} = {value}") |
| | 154 | + |
| | 155 | + if group.functions: |
| | 156 | + print(f" Functions ({len(group.functions)}):") |
| | 157 | + for key, value in group.functions.items(): |
| | 158 | + print(f" {key} = {value}") |
| | 159 | + |
| | 160 | + except FileNotFoundError: |
| | 161 | + print(f"Config file not found: {config_path}") |
| | 162 | + print( |
| | 163 | + "Use 'shtick add' to create entries or 'shtick generate' with a config file" |
| | 164 | + ) |
| | 165 | + except Exception as e: |
| | 166 | + print(f"Error: {e}") |
| | 167 | + sys.exit(1) |
| | 168 | + |
| | 169 | + |
| | 170 | +def main(): |
| | 171 | + """Main CLI entry point""" |
| | 172 | + parser = argparse.ArgumentParser( |
| | 173 | + description="shtick - Generate shell configuration files from TOML" |
| | 174 | + ) |
| | 175 | + |
| | 176 | + subparsers = parser.add_subparsers(dest="command", help="Available commands") |
| | 177 | + |
| | 178 | + # Generate command |
| | 179 | + gen_parser = subparsers.add_parser( |
| | 180 | + "generate", help="Generate shell files from config" |
| | 181 | + ) |
| | 182 | + gen_parser.add_argument("config", nargs="?", help="Path to config TOML file") |
| | 183 | + |
| | 184 | + # Add command |
| | 185 | + add_parser = subparsers.add_parser("add", help="Add an item to config") |
| | 186 | + add_parser.add_argument( |
| | 187 | + "type", choices=["alias", "env", "function"], help="Type of item to add" |
| | 188 | + ) |
| | 189 | + add_parser.add_argument("group", help="Group name") |
| | 190 | + add_parser.add_argument("assignment", help="Assignment in format key=value") |
| | 191 | + |
| | 192 | + # Remove command |
| | 193 | + rm_parser = subparsers.add_parser("remove", help="Remove an item from config") |
| | 194 | + rm_parser.add_argument( |
| | 195 | + "type", choices=["alias", "env", "function"], help="Type of item to remove" |
| | 196 | + ) |
| | 197 | + rm_parser.add_argument("group", help="Group name") |
| | 198 | + rm_parser.add_argument("search", help="Search term (fuzzy match)") |
| | 199 | + |
| | 200 | + # List command |
| | 201 | + list_parser = subparsers.add_parser("list", help="List current configuration") |
| | 202 | + |
| | 203 | + # Shells command (bonus) |
| | 204 | + shells_parser = subparsers.add_parser("shells", help="List supported shells") |
| | 205 | + |
| | 206 | + args = parser.parse_args() |
| | 207 | + |
| | 208 | + if not args.command: |
| | 209 | + parser.print_help() |
| | 210 | + sys.exit(1) |
| | 211 | + |
| | 212 | + if args.command == "generate": |
| | 213 | + cmd_generate(args) |
| | 214 | + elif args.command == "add": |
| | 215 | + cmd_add(args) |
| | 216 | + elif args.command == "remove": |
| | 217 | + cmd_remove(args) |
| | 218 | + elif args.command == "list": |
| | 219 | + cmd_list(args) |
| | 220 | + elif args.command == "shells": |
| | 221 | + print("Supported shells:") |
| | 222 | + for shell in sorted(get_supported_shells()): |
| | 223 | + print(f" {shell}") |
| | 224 | + |
| | 225 | + |
| | 226 | +if __name__ == "__main__": |
| | 227 | + main() |