gitacc-switcher 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gitacc_switcher/cli.py ADDED
@@ -0,0 +1,426 @@
1
+ """Main CLI entry point with argparse commands."""
2
+
3
+ import argparse
4
+ import sys
5
+ from typing import Optional
6
+
7
+ try:
8
+ import argcomplete
9
+ except ImportError:
10
+ argcomplete = None
11
+
12
+ from . import __version__
13
+ from .account_manager import AccountManager
14
+ from .completion import get_account_names
15
+ from .utils import echo_color, get_ssh_key_types
16
+
17
+
18
+ class CLI:
19
+ """Command-line interface for Git Account Switcher."""
20
+
21
+ def __init__(self):
22
+ self.account_manager = AccountManager()
23
+ self.parser = self._create_parser()
24
+
25
+ def _create_parser(self) -> argparse.ArgumentParser:
26
+ """Create and configure the argument parser."""
27
+ parser = argparse.ArgumentParser(
28
+ prog="gitacc",
29
+ description="Git Account Switcher - Manage multiple Git SSH accounts easily",
30
+ formatter_class=argparse.RawDescriptionHelpFormatter,
31
+ epilog=self._get_examples(),
32
+ )
33
+ parser.add_argument(
34
+ "--version", action="version", version=f"%(prog)s {__version__}"
35
+ )
36
+
37
+ subparsers = parser.add_subparsers(
38
+ dest="command",
39
+ help="Available commands",
40
+ metavar="COMMAND",
41
+ )
42
+
43
+ self._add_add_command(subparsers)
44
+ self._add_remove_command(subparsers)
45
+ self._add_switch_command(subparsers)
46
+ self._add_list_command(subparsers)
47
+ self._add_logout_command(subparsers)
48
+ self._add_init_command(subparsers)
49
+ self._add_verify_command(subparsers)
50
+ self._add_update_command(subparsers)
51
+ self._add_autocomplete_command(subparsers)
52
+
53
+ # Include account names in top-level completion so `gitacc <TAB>` works for
54
+ # the `gitacc <account>` shorthand, not just for subcommand names.
55
+ if argcomplete:
56
+ subparsers.completer = lambda prefix, **kwargs: (
57
+ list(subparsers.choices.keys()) + get_account_names()
58
+ )
59
+
60
+ return parser
61
+
62
+ def _get_examples(self) -> str:
63
+ """Get usage examples string."""
64
+ key_types = ", ".join(get_ssh_key_types())
65
+ return f"""
66
+ Examples:
67
+ gitacc add Add a new Git account
68
+ gitacc add --type ed25519 Add account with specific SSH key type
69
+ Available types: {key_types}
70
+ gitacc switch myaccount Switch to an account
71
+ gitacc myaccount Switch to an account (short form)
72
+ gitacc remove myaccount Remove an account
73
+ gitacc list List all registered accounts
74
+ gitacc logout Logout current account
75
+ gitacc init myaccount Initialize repo with account validation
76
+ gitacc verify Verify current account matches repo
77
+ gitacc update myaccount Update Git name for an account
78
+ """
79
+
80
+ def _add_add_command(self, subparsers: argparse._SubParsersAction) -> None:
81
+ """Add the 'add' command parser."""
82
+ parser = subparsers.add_parser(
83
+ "add",
84
+ help="Add a new Git account",
85
+ description="Add a new Git account with SSH key generation",
86
+ )
87
+ parser.add_argument(
88
+ "-t",
89
+ "--type",
90
+ dest="key_type",
91
+ default="rsa",
92
+ choices=get_ssh_key_types(),
93
+ help="SSH key type (default: rsa). Available: dsa, ecdsa, ecdsa-sk, ed25519, ed25519-sk, rsa",
94
+ )
95
+ parser.set_defaults(func=self._handle_add)
96
+
97
+ def _add_remove_command(self, subparsers: argparse._SubParsersAction) -> None:
98
+ """Add the 'remove' command parser."""
99
+ parser = subparsers.add_parser(
100
+ "remove",
101
+ help="Remove a Git account",
102
+ description="Remove a Git account and its SSH keys",
103
+ )
104
+ account_arg = parser.add_argument(
105
+ "account_name",
106
+ nargs="?",
107
+ help="Account name to remove (will prompt if not provided)",
108
+ )
109
+ if argcomplete:
110
+ account_arg.completer = lambda **kwargs: get_account_names()
111
+ parser.set_defaults(func=self._handle_remove)
112
+
113
+ def _add_switch_command(self, subparsers: argparse._SubParsersAction) -> None:
114
+ """Add the 'switch' command parser."""
115
+ parser = subparsers.add_parser(
116
+ "switch",
117
+ help="Switch to a Git account",
118
+ description="Switch to a registered Git account",
119
+ )
120
+ account_arg = parser.add_argument(
121
+ "account_name",
122
+ help="Account name to switch to",
123
+ )
124
+ if argcomplete:
125
+ account_arg.completer = lambda **kwargs: get_account_names()
126
+ parser.set_defaults(func=self._handle_switch)
127
+
128
+ def _add_list_command(self, subparsers: argparse._SubParsersAction) -> None:
129
+ """Add the 'list' command parser."""
130
+ parser = subparsers.add_parser(
131
+ "list",
132
+ help="List all registered accounts",
133
+ description="Display all registered Git accounts",
134
+ )
135
+ parser.set_defaults(func=self._handle_list)
136
+
137
+ def _add_logout_command(self, subparsers: argparse._SubParsersAction) -> None:
138
+ """Add the 'logout' command parser."""
139
+ parser = subparsers.add_parser(
140
+ "logout",
141
+ help="Logout current Git account",
142
+ description="Logout from the current Git account",
143
+ )
144
+ parser.set_defaults(func=self._handle_logout)
145
+
146
+ def _add_init_command(self, subparsers: argparse._SubParsersAction) -> None:
147
+ """Add the 'init' command parser."""
148
+ parser = subparsers.add_parser(
149
+ "init",
150
+ help="Initialize repository with account validation",
151
+ description="Set expected account for repository and install pre-commit hook",
152
+ )
153
+ account_arg = parser.add_argument(
154
+ "account_name",
155
+ help="Account name to set as expected for this repository",
156
+ )
157
+ if argcomplete:
158
+ account_arg.completer = lambda **kwargs: get_account_names()
159
+ parser.set_defaults(func=self._handle_init)
160
+
161
+ def _add_verify_command(self, subparsers: argparse._SubParsersAction) -> None:
162
+ """Add the 'verify' command parser."""
163
+ parser = subparsers.add_parser(
164
+ "verify",
165
+ help="Verify current account matches repository",
166
+ description="Check if current Git account matches the expected account for the repository",
167
+ )
168
+ parser.set_defaults(func=self._handle_verify)
169
+
170
+ def _add_update_command(self, subparsers: argparse._SubParsersAction) -> None:
171
+ """Add the 'update' command parser."""
172
+ parser = subparsers.add_parser(
173
+ "update",
174
+ help="Update Git name and/or email for an account",
175
+ description="Update the Git name and/or email for an existing account",
176
+ )
177
+ account_arg = parser.add_argument(
178
+ "account_name",
179
+ help="Account identifier to update",
180
+ )
181
+ parser.add_argument(
182
+ "--name",
183
+ dest="new_git_name",
184
+ help="New Git name (will prompt if not provided)",
185
+ )
186
+ parser.add_argument(
187
+ "--email",
188
+ dest="new_email",
189
+ help="New email address (will prompt if not provided)",
190
+ )
191
+ if argcomplete:
192
+ account_arg.completer = lambda **kwargs: get_account_names()
193
+ parser.set_defaults(func=self._handle_update)
194
+
195
+ def _add_autocomplete_command(self, subparsers: argparse._SubParsersAction) -> None:
196
+ """Add the 'autocomplete' command parser."""
197
+ parser = subparsers.add_parser(
198
+ "autocomplete",
199
+ help="Install shell autocomplete",
200
+ description="Install shell autocomplete for gitacc command",
201
+ )
202
+ subparsers_autocomplete = parser.add_subparsers(
203
+ dest="autocomplete_command",
204
+ help="Autocomplete commands",
205
+ )
206
+
207
+ install_parser = subparsers_autocomplete.add_parser(
208
+ "install",
209
+ help="Install autocomplete for your shell",
210
+ description="Install autocomplete for bash/zsh",
211
+ )
212
+ install_parser.set_defaults(func=self._handle_autocomplete_install)
213
+
214
+ def _handle_autocomplete_install(self, args: argparse.Namespace) -> int:
215
+ """Handle the 'autocomplete install' command."""
216
+ if not argcomplete:
217
+ echo_color("r", "argcomplete is not installed!")
218
+ echo_color(
219
+ "y",
220
+ "This should have been installed automatically with gitacc-switcher.",
221
+ )
222
+ echo_color("y", "Please reinstall the package:")
223
+ echo_color("b", " pip install --upgrade gitacc-switcher")
224
+ echo_color("y", "Or if developing locally:")
225
+ echo_color("b", " pip install -e .")
226
+ return 1
227
+
228
+ import os
229
+ import shutil
230
+
231
+ shell = os.environ.get("SHELL", "")
232
+ shell_name = os.path.basename(shell) if shell else ""
233
+
234
+ if shell_name in ["bash", "zsh"]:
235
+ try:
236
+ # Get the completion script
237
+ completion_script = argcomplete.shellcode(["gitacc"], shell=shell_name)
238
+
239
+ # Determine shell config file
240
+ if shell_name == "bash":
241
+ config_file = os.path.expanduser("~/.bashrc")
242
+ # Check for .bash_profile on macOS
243
+ if sys.platform == "darwin" and os.path.exists(
244
+ os.path.expanduser("~/.bash_profile")
245
+ ):
246
+ config_file = os.path.expanduser("~/.bash_profile")
247
+ else: # zsh
248
+ config_file = os.path.expanduser("~/.zshrc")
249
+
250
+ # Check if already installed
251
+ if os.path.exists(config_file):
252
+ with open(config_file, "r") as f:
253
+ if "register-python-argcomplete gitacc" in f.read():
254
+ echo_color(
255
+ "y", f"Autocomplete already installed in {config_file}"
256
+ )
257
+ echo_color(
258
+ "y",
259
+ f"To activate in this session: source {config_file}",
260
+ )
261
+ return 0
262
+
263
+ # Add to config file
264
+ with open(config_file, "a") as f:
265
+ f.write("\n# Git Account Switcher autocomplete\n")
266
+ f.write(completion_script)
267
+ f.write("\n")
268
+
269
+ echo_color("g", f"Autocomplete installed successfully!")
270
+ echo_color("g", f"Added to {config_file}")
271
+
272
+ # Try to source it automatically for current shell
273
+ # Note: We can't modify the parent shell, but we can validate and provide instructions
274
+ try:
275
+ import subprocess
276
+
277
+ # Validate the installation works
278
+ result = subprocess.run(
279
+ [shell, "-c", f"source {config_file} && echo 'OK'"],
280
+ capture_output=True,
281
+ text=True,
282
+ timeout=2,
283
+ )
284
+ if result.returncode == 0:
285
+ echo_color("g", "✓ Installation validated!")
286
+ echo_color(
287
+ "y",
288
+ "Autocomplete will work automatically in new shell sessions.",
289
+ )
290
+ echo_color(
291
+ "y", f"To activate in this session: source {config_file}"
292
+ )
293
+ else:
294
+ echo_color("y", f"To activate: source {config_file}")
295
+ except Exception:
296
+ echo_color("y", f"To activate: source {config_file}")
297
+
298
+ return 0
299
+
300
+ except Exception as e:
301
+ echo_color("r", f"Failed to install autocomplete: {e}")
302
+ echo_color("y", "You can manually install by running:")
303
+ echo_color("b", f' eval "$(register-python-argcomplete gitacc)"')
304
+ return 1
305
+ else:
306
+ echo_color("y", f"Shell '{shell_name}' detected.")
307
+ echo_color("y", "Automatic installation is supported for bash and zsh.")
308
+ echo_color("y", "For other shells, run manually:")
309
+ echo_color("b", ' eval "$(register-python-argcomplete gitacc)"')
310
+ return 0
311
+
312
+ def _handle_add(self, args: argparse.Namespace) -> int:
313
+ """Handle the 'add' command."""
314
+ success = self.account_manager.add_account(key_type=args.key_type)
315
+ return 0 if success else 1
316
+
317
+ def _handle_remove(self, args: argparse.Namespace) -> int:
318
+ """Handle the 'remove' command."""
319
+ success = self.account_manager.remove_account(account_name=args.account_name)
320
+ return 0 if success else 1
321
+
322
+ def _handle_switch(self, args: argparse.Namespace) -> int:
323
+ """Handle the 'switch' command."""
324
+ success = self.account_manager.switch_account(args.account_name)
325
+ return 0 if success else 1
326
+
327
+ def _handle_list(self, args: argparse.Namespace) -> int:
328
+ """Handle the 'list' command."""
329
+ accounts = self.account_manager.list_accounts_detailed()
330
+ if not accounts:
331
+ echo_color("y", "No accounts registered yet.")
332
+ return 0
333
+
334
+ current_config = self.account_manager.config_manager.get_current_git_config()
335
+ current_name = current_config.get("name")
336
+ current_email = current_config.get("email")
337
+
338
+ echo_color("g", "Registered accounts:")
339
+ for account_name, account_info in accounts.items():
340
+ git_name = account_info.get("name", account_name)
341
+ email = account_info.get("email", "N/A")
342
+ is_active = bool(
343
+ current_name
344
+ and current_email
345
+ and git_name == current_name
346
+ and email == current_email
347
+ )
348
+ marker = "*" if is_active else "-"
349
+ label = (
350
+ f"{account_name} → Git name: {git_name}"
351
+ if git_name != account_name
352
+ else account_name
353
+ )
354
+ suffix = " (active)" if is_active else ""
355
+ print(f" {marker} {label} ({email}){suffix}")
356
+
357
+ return 0
358
+
359
+ def _handle_logout(self, args: argparse.Namespace) -> int:
360
+ """Handle the 'logout' command."""
361
+ success = self.account_manager.logout()
362
+ return 0 if success else 1
363
+
364
+ def _handle_init(self, args: argparse.Namespace) -> int:
365
+ """Handle the 'init' command."""
366
+ success = self.account_manager.init_repo(args.account_name)
367
+ return 0 if success else 1
368
+
369
+ def _handle_verify(self, args: argparse.Namespace) -> int:
370
+ """Handle the 'verify' command."""
371
+ success = self.account_manager.verify_account()
372
+ return 0 if success else 1
373
+
374
+ def _handle_update(self, args: argparse.Namespace) -> int:
375
+ """Handle the 'update' command."""
376
+ success = self.account_manager.update_account(
377
+ args.account_name, args.new_git_name, args.new_email
378
+ )
379
+ return 0 if success else 1
380
+
381
+ def _handle_account_name_shortcut(self, account_name: str) -> int:
382
+ """Handle account name as shortcut for switch command."""
383
+ success = self.account_manager.switch_account(account_name)
384
+ return 0 if success else 1
385
+
386
+ def run(self) -> int:
387
+ """Run the CLI application.
388
+
389
+ Returns:
390
+ Exit code (0 for success, non-zero for failure)
391
+ """
392
+ args = self.parser.parse_args()
393
+
394
+ # Handle account name shortcut (gitacc <account_name>)
395
+ if not args.command and len(sys.argv) > 1:
396
+ account_name = sys.argv[1]
397
+ if account_name not in ["-h", "--help", "--version"]:
398
+ if not self.account_manager.account_exists(account_name):
399
+ echo_color("r", f'Unknown command or account: "{account_name}"')
400
+ self.parser.print_help()
401
+ return 1
402
+ return self._handle_account_name_shortcut(account_name)
403
+
404
+ # Handle commands
405
+ if hasattr(args, "func"):
406
+ return args.func(args)
407
+
408
+ # No command provided, show help
409
+ self.parser.print_help()
410
+ return 0
411
+
412
+
413
+ def main() -> None:
414
+ """Main entry point for the CLI."""
415
+ cli = CLI()
416
+
417
+ # Enable argcomplete if available
418
+ if argcomplete:
419
+ argcomplete.autocomplete(cli.parser)
420
+
421
+ exit_code = cli.run()
422
+ sys.exit(exit_code)
423
+
424
+
425
+ if __name__ == "__main__":
426
+ main()
@@ -0,0 +1,17 @@
1
+ """Completion functions for shell autocomplete."""
2
+
3
+ from typing import List
4
+ from .config_manager import ConfigManager
5
+
6
+
7
+ def get_account_names() -> List[str]:
8
+ """Get list of account names for completion.
9
+
10
+ Returns:
11
+ List of account names
12
+ """
13
+ try:
14
+ config_manager = ConfigManager()
15
+ return config_manager.list_account_names()
16
+ except Exception:
17
+ return []