tenseleyflow/shtick / b2cc963

Browse files

bug fixes

Authored by mfwolffe <wolffemf@dukes.jmu.edu>
SHA
b2cc9633491d8594413abf149698856a80604ab1
Parents
f9006e1
Tree
0b4ee20

3 changed files

StatusFile+-
M .gitignore 1 0
A src/shtick/cli.py 502 0
D src/shtick/shtick.py 0 227
.gitignoremodified
@@ -1,4 +1,5 @@
11
 dist/*
22
 build/*
3
+.DS_Store
34
 **/*/*.egg-info/*
45
 **/*/__pycache__/*
src/shtick/cli.pyadded
@@ -0,0 +1,502 @@
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
+def cmd_generate(args) -> None:
18
+    """Generate shell files from config"""
19
+    config_path = args.config or Config.get_default_config_path()
20
+    
21
+    try:
22
+        config = Config(config_path)
23
+        config.load()
24
+        
25
+        generator = Generator()
26
+        generator.generate_all(config)
27
+        
28
+    except FileNotFoundError as e:
29
+        print(f"Error: {e}")
30
+        print(f"Create a config file at {config_path} first")
31
+        sys.exit(1)
32
+    except Exception as e:
33
+        print(f"Error: {e}")
34
+        sys.exit(1)
35
+
36
+def cmd_add(args) -> None:
37
+    """Add an item to the config"""
38
+    if '=' not in args.assignment:
39
+        print("Error: Assignment must be in format key=value")
40
+        sys.exit(1)
41
+    
42
+    key, value = args.assignment.split('=', 1)
43
+    key = key.strip()
44
+    value = value.strip()
45
+    
46
+    if not key or not value:
47
+        print("Error: Both key and value must be non-empty")
48
+        sys.exit(1)
49
+    
50
+    config_path = Config.get_default_config_path()
51
+    
52
+    try:
53
+        config = Config(config_path)
54
+        # Try to load existing config, create empty if doesn't exist
55
+        try:
56
+            config.load()
57
+        except FileNotFoundError:
58
+            print(f"Creating new config file at {config_path}")
59
+        
60
+        config.add_item(args.type, args.group, key, value)
61
+        config.save()
62
+        
63
+        print(f"Added {args.type} '{key}' = '{value}' to group '{args.group}'")
64
+        
65
+    except Exception as e:
66
+        print(f"Error: {e}")
67
+        sys.exit(1)
68
+
69
+def cmd_remove(args) -> None:
70
+    """Remove an item from the config"""
71
+    config_path = Config.get_default_config_path()
72
+    
73
+    try:
74
+        config = Config(config_path)
75
+        config.load()
76
+        
77
+        # Find matching items
78
+        matches = config.find_items(args.type, args.group, args.search)
79
+        
80
+        if not matches:
81
+            print(f"No {args.type} items matching '{args.search}' found in group '{args.group}'")
82
+            return
83
+        
84
+        if len(matches) == 1:
85
+            # Exact match, remove it
86
+            item = matches[0]
87
+            if config.remove_item(args.type, args.group, item):
88
+                config.save()
89
+                print(f"Removed {args.type} '{item}' from group '{args.group}'")
90
+            else:
91
+                print(f"Failed to remove {args.type} '{item}'")
92
+        else:
93
+            # Multiple matches, ask for confirmation
94
+            print(f"Found {len(matches)} matches:")
95
+            for i, item in enumerate(matches, 1):
96
+                print(f"  {i}. {item}")
97
+            
98
+            try:
99
+                choice = input("Enter number to remove (or 'q' to quit): ").strip()
100
+                if choice.lower() == 'q':
101
+                    print("Cancelled")
102
+                    return
103
+                
104
+                idx = int(choice) - 1
105
+                if 0 <= idx < len(matches):
106
+                    item = matches[idx]
107
+                    if config.remove_item(args.type, args.group, item):
108
+                        config.save()
109
+                        print(f"Removed {args.type} '{item}' from group '{args.group}'")
110
+                    else:
111
+                        print(f"Failed to remove {args.type} '{item}'")
112
+                else:
113
+                    print("Invalid choice")
114
+            except (ValueError, KeyboardInterrupt):
115
+                print("\nCancelled")
116
+    
117
+    except FileNotFoundError:
118
+        print(f"Config file not found: {config_path}")
119
+        sys.exit(1)
120
+    except Exception as e:
121
+        print(f"Error: {e}")
122
+        sys.exit(1)
123
+
124
+def cmd_activate(args) -> None:
125
+    """Activate a group"""
126
+    config_path = Config.get_default_config_path()
127
+    
128
+    try:
129
+        config = Config(config_path)
130
+        config.load()
131
+        
132
+        if args.group == 'persistent':
133
+            print("Error: 'persistent' group is always active and cannot be manually activated")
134
+            return
135
+        
136
+        if config.activate_group(args.group):
137
+            # Regenerate loader to include newly activated group
138
+            from shtick.generator import Generator
139
+            generator = Generator()
140
+            generator.generate_loader(config)
141
+            
142
+            print(f"Activated group '{args.group}'")
143
+            print("Changes are now active in new shell sessions")
144
+        else:
145
+            print(f"Error: Group '{args.group}' not found in configuration")
146
+            available = [g.name for g in config.get_regular_groups()]
147
+            if available:
148
+                print(f"Available groups: {', '.join(available)}")
149
+    
150
+    except FileNotFoundError:
151
+        print(f"Config file not found: {config_path}")
152
+        print("Run 'shtick generate' first or add some items with 'shtick add'")
153
+        sys.exit(1)
154
+    except Exception as e:
155
+        print(f"Error: {e}")
156
+        sys.exit(1)
157
+
158
+def cmd_deactivate(args) -> None:
159
+    """Deactivate a group"""
160
+    config_path = Config.get_default_config_path()
161
+    
162
+    try:
163
+        config = Config(config_path)
164
+        config.load()
165
+        
166
+        if args.group == 'persistent':
167
+            print("Error: 'persistent' group cannot be deactivated")
168
+            return
169
+        
170
+        if config.deactivate_group(args.group):
171
+            # Regenerate loader to exclude deactivated group
172
+            from shtick.generator import Generator
173
+            generator = Generator()
174
+            generator.generate_loader(config)
175
+            
176
+            print(f"Deactivated group '{args.group}'")
177
+            print("Changes will take effect in new shell sessions")
178
+        else:
179
+            print(f"Group '{args.group}' was not active")
180
+    
181
+    except FileNotFoundError:
182
+        print(f"Config file not found: {config_path}")
183
+        sys.exit(1)
184
+    except Exception as e:
185
+        print(f"Error: {e}")
186
+        sys.exit(1)
187
+
188
+def cmd_list(args) -> None:
189
+    """List current configuration"""
190
+    config_path = Config.get_default_config_path()
191
+    
192
+    try:
193
+        config = Config(config_path)
194
+        config.load()
195
+        
196
+        if not config.groups:
197
+            print("No groups configured")
198
+            return
199
+        
200
+        persistent_group = config.get_persistent_group()
201
+        regular_groups = config.get_regular_groups()
202
+        active_groups = config.load_active_groups()
203
+        
204
+        if args.long:
205
+            # Long format - detailed line-by-line
206
+            _print_detailed_list(persistent_group, regular_groups, active_groups)
207
+        else:
208
+            # Default tabular format
209
+            _print_tabular_list(persistent_group, regular_groups, active_groups)
210
+    
211
+    except FileNotFoundError:
212
+        print(f"Config file not found: {config_path}")
213
+        print("Use 'shtick add' to create entries or 'shtick generate' with a config file")
214
+    except Exception as e:
215
+        print(f"Error: {e}")
216
+        sys.exit(1)
217
+
218
+def _print_detailed_list(persistent_group, regular_groups, active_groups) -> None:
219
+    """Print detailed line-by-line list format"""
220
+    # Show persistent group first
221
+    if persistent_group:
222
+        print("Group: persistent (always active)")
223
+        if persistent_group.aliases:
224
+            print(f"  Aliases ({len(persistent_group.aliases)}):")
225
+            for key, value in persistent_group.aliases.items():
226
+                print(f"    {key} = {value}")
227
+        if persistent_group.env_vars:
228
+            print(f"  Environment Variables ({len(persistent_group.env_vars)}):")
229
+            for key, value in persistent_group.env_vars.items():
230
+                print(f"    {key} = {value}")
231
+        if persistent_group.functions:
232
+            print(f"  Functions ({len(persistent_group.functions)}):")
233
+            for key, value in persistent_group.functions.items():
234
+                print(f"    {key} = {value}")
235
+        print()
236
+    
237
+    # Show regular groups
238
+    for group in regular_groups:
239
+        status = " (ACTIVE)" if group.name in active_groups else " (inactive)"
240
+        print(f"Group: {group.name}{status}")
241
+        
242
+        if group.aliases:
243
+            print(f"  Aliases ({len(group.aliases)}):")
244
+            for key, value in group.aliases.items():
245
+                print(f"    {key} = {value}")
246
+        
247
+        if group.env_vars:
248
+            print(f"  Environment Variables ({len(group.env_vars)}):")
249
+            for key, value in group.env_vars.items():
250
+                print(f"    {key} = {value}")
251
+        
252
+        if group.functions:
253
+            print(f"  Functions ({len(group.functions)}):")
254
+            for key, value in group.functions.items():
255
+                print(f"    {key} = {value}")
256
+        print()
257
+
258
+def _print_tabular_list(persistent_group, regular_groups, active_groups) -> None:
259
+    """Print compact tabular list format"""
260
+    # Collect all items for tabular display
261
+    items = []
262
+    
263
+    # Add persistent items
264
+    if persistent_group:
265
+        for key, value in persistent_group.aliases.items():
266
+            items.append(("persistent", "alias", key, value, "PERSISTENT"))
267
+        for key, value in persistent_group.env_vars.items():
268
+            items.append(("persistent", "env", key, value, "PERSISTENT"))
269
+        for key, value in persistent_group.functions.items():
270
+            items.append(("persistent", "function", key, value, "PERSISTENT"))
271
+    
272
+    # Add regular group items
273
+    for group in regular_groups:
274
+        status = "ACTIVE" if group.name in active_groups else "inactive"
275
+        
276
+        for key, value in group.aliases.items():
277
+            items.append((group.name, "alias", key, value, status))
278
+        for key, value in group.env_vars.items():
279
+            items.append((group.name, "env", key, value, status))
280
+        for key, value in group.functions.items():
281
+            items.append((group.name, "function", key, value, status))
282
+    
283
+    if not items:
284
+        print("No items configured")
285
+        return
286
+    
287
+    # Calculate column widths
288
+    max_group = max(len(item[0]) for item in items)
289
+    max_type = max(len(item[1]) for item in items)
290
+    max_key = max(len(item[2]) for item in items)
291
+    max_value = max(min(len(item[3]), 50) for item in items)  # Limit value column width
292
+    max_status = max(len(item[4]) for item in items)
293
+    
294
+    # Ensure minimum widths
295
+    max_group = max(max_group, 5)  # "Group"
296
+    max_type = max(max_type, 4)    # "Type"
297
+    max_key = max(max_key, 3)      # "Key"
298
+    max_value = max(max_value, 5)  # "Value"
299
+    max_status = max(max_status, 6) # "Status"
300
+    
301
+    # Print header
302
+    header = f"{'Group':<{max_group}} {'Type':<{max_type}} {'Key':<{max_key}} {'Value':<{max_value}} {'Status':<{max_status}}"
303
+    print(header)
304
+    print("-" * len(header))
305
+    
306
+    # Print items
307
+    for group, item_type, key, value, status in items:
308
+        # Truncate long values with ellipsis
309
+        display_value = value if len(value) <= max_value else value[:max_value-3] + "..."
310
+        
311
+        print(f"{group:<{max_group}} {item_type:<{max_type}} {key:<{max_key}} {display_value:<{max_value}} {status:<{max_status}}")
312
+    
313
+    # Print summary
314
+    print()
315
+    total_items = len(items)
316
+    active_items = len([item for item in items if item[4] in ["ACTIVE", "PERSISTENT"]])
317
+    print(f"Total: {total_items} items ({active_items} active)")
318
+    
319
+    # Show available commands
320
+    print()
321
+    print("Use 'shtick list -l' for detailed view")
322
+    if any(item[4] == "inactive" for item in items):
323
+        inactive_groups = set(item[0] for item in items if item[4] == "inactive")
324
+        print(f"Activate groups with: shtick activate <group>")
325
+        print(f"Inactive groups: {', '.join(sorted(inactive_groups))}")
326
+
327
+def cmd_shells(args) -> None:
328
+    """List supported shells"""
329
+    shells = sorted(get_supported_shells())
330
+    
331
+    if args.long:
332
+        # Long format - one per line with descriptions
333
+        print("Supported shells:")
334
+        for shell in shells:
335
+            print(f"  {shell}")
336
+    else:
337
+        # Default columnar format (like ls)
338
+        _print_shells_columns(shells)
339
+
340
+def _print_shells_columns(shells) -> None:
341
+    """Print shells in columns like ls output"""
342
+    if not shells:
343
+        print("No shells configured")
344
+        return
345
+    
346
+    # Try to get terminal width, fallback to 80
347
+    try:
348
+        import shutil
349
+        terminal_width = shutil.get_terminal_size().columns
350
+    except:
351
+        terminal_width = 80
352
+    
353
+    # Find the longest shell name
354
+    max_shell_length = max(len(shell) for shell in shells)
355
+    
356
+    # Add some padding
357
+    column_width = max_shell_length + 2
358
+    
359
+    # Calculate how many columns we can fit
360
+    columns = max(1, terminal_width // column_width)
361
+    
362
+    # Calculate number of rows needed
363
+    rows = (len(shells) + columns - 1) // columns
364
+    
365
+    print(f"Supported shells ({len(shells)} total):")
366
+    print()
367
+    
368
+    # Print shells in columns
369
+    for row in range(rows):
370
+        line = ""
371
+        for col in range(columns):
372
+            index = row + col * rows
373
+            if index < len(shells):
374
+                shell = shells[index]
375
+                line += f"{shell:<{column_width}}"
376
+        print(line.rstrip())
377
+
378
+def cmd_status(args) -> None:
379
+    """Show status of groups and active state"""
380
+    config_path = Config.get_default_config_path()
381
+    
382
+    try:
383
+        config = Config(config_path)
384
+        config.load()
385
+        
386
+        persistent_group = config.get_persistent_group()
387
+        regular_groups = config.get_regular_groups()
388
+        active_groups = config.load_active_groups()
389
+        
390
+        print("Shtick Status")
391
+        print("=" * 40)
392
+        
393
+        # Show persistent group
394
+        if persistent_group:
395
+            total_persistent = len(persistent_group.aliases) + len(persistent_group.env_vars) + len(persistent_group.functions)
396
+            print(f"Persistent (always active): {total_persistent} items")
397
+        else:
398
+            print("Persistent: No items")
399
+        
400
+        print()
401
+        
402
+        # Show regular groups
403
+        if regular_groups:
404
+            print("Available Groups:")
405
+            for group in regular_groups:
406
+                status = "ACTIVE" if group.name in active_groups else "inactive"
407
+                total_items = len(group.aliases) + len(group.env_vars) + len(group.functions)
408
+                print(f"  {group.name}: {total_items} items ({status})")
409
+        else:
410
+            print("No regular groups configured")
411
+        
412
+        print()
413
+        
414
+        # Show summary
415
+        if active_groups:
416
+            print(f"Currently active: {', '.join(active_groups)}")
417
+        else:
418
+            print("No groups currently active")
419
+        
420
+        print()
421
+        print("To activate a group: shtick activate <group>")
422
+        print("To deactivate a group: shtick deactivate <group>")
423
+    
424
+    except FileNotFoundError:
425
+        print(f"Config file not found: {config_path}")
426
+        print("No configuration exists yet")
427
+    except Exception as e:
428
+        print(f"Error: {e}")
429
+        sys.exit(1)
430
+
431
+def main():
432
+    """Main CLI entry point"""
433
+    parser = argparse.ArgumentParser(
434
+        description="shtick - Generate shell configuration files from TOML"
435
+    )
436
+    
437
+    subparsers = parser.add_subparsers(dest='command', help='Available commands')
438
+    
439
+    # Generate command
440
+    gen_parser = subparsers.add_parser('generate', help='Generate shell files from config')
441
+    gen_parser.add_argument('config', nargs='?', help='Path to config TOML file')
442
+    
443
+    # Add command  
444
+    add_parser = subparsers.add_parser('add', help='Add an item to config')
445
+    add_parser.add_argument('type', choices=['alias', 'env', 'function'], 
446
+                           help='Type of item to add')
447
+    add_parser.add_argument('group', help='Group name')
448
+    add_parser.add_argument('assignment', help='Assignment in format key=value')
449
+    
450
+    # Remove command
451
+    rm_parser = subparsers.add_parser('remove', help='Remove an item from config')
452
+    rm_parser.add_argument('type', choices=['alias', 'env', 'function'],
453
+                          help='Type of item to remove')
454
+    rm_parser.add_argument('group', help='Group name')
455
+    rm_parser.add_argument('search', help='Search term (fuzzy match)')
456
+    
457
+    # Activate command
458
+    activate_parser = subparsers.add_parser('activate', help='Activate a group')
459
+    activate_parser.add_argument('group', help='Group name to activate')
460
+    
461
+    # Deactivate command  
462
+    deactivate_parser = subparsers.add_parser('deactivate', help='Deactivate a group')
463
+    deactivate_parser.add_argument('group', help='Group name to deactivate')
464
+    
465
+    # Status command
466
+    status_parser = subparsers.add_parser('status', help='Show status of groups')
467
+    
468
+    # List command
469
+    list_parser = subparsers.add_parser('list', help='List current configuration')
470
+    list_parser.add_argument('-l', '--long', action='store_true', 
471
+                            help='Show detailed line-by-line format instead of table')
472
+    
473
+    # Shells command
474
+    shells_parser = subparsers.add_parser('shells', help='List supported shells')
475
+    shells_parser.add_argument('-l', '--long', action='store_true',
476
+                              help='Show one shell per line instead of columns')
477
+    
478
+    args = parser.parse_args()
479
+    
480
+    if not args.command:
481
+        parser.print_help()
482
+        sys.exit(1)
483
+    
484
+    if args.command == 'generate':
485
+        cmd_generate(args)
486
+    elif args.command == 'add':
487
+        cmd_add(args)
488
+    elif args.command == 'remove':
489
+        cmd_remove(args)
490
+    elif args.command == 'activate':
491
+        cmd_activate(args)
492
+    elif args.command == 'deactivate':
493
+        cmd_deactivate(args)
494
+    elif args.command == 'status':
495
+        cmd_status(args)
496
+    elif args.command == 'list':
497
+        cmd_list(args)
498
+    elif args.command == 'shells':
499
+        cmd_shells(args)
500
+
501
+if __name__ == '__main__':
502
+    main()
src/shtick/shtick.pydeleted
@@ -1,227 +0,0 @@
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()