diff --git a/README.md b/README.md index 757bc23..d851dce 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,9 @@ mycli self uninstall # Uninstall mycli mycli self info # Show installation info # Configuration (config.json) - 增删改查 +# 方式1: 交互式向导(推荐,无需记忆命令) +mycli wizard # 启动交互式配置向导 +# 方式2: 命令行直接操作 mycli config show # Show full config mycli config list # List top-level sections mycli config get paths # Get paths section diff --git a/mycli/__pycache__/cli.cpython-310.pyc b/mycli/__pycache__/cli.cpython-310.pyc index a4b3076..a540f68 100644 Binary files a/mycli/__pycache__/cli.cpython-310.pyc and b/mycli/__pycache__/cli.cpython-310.pyc differ diff --git a/mycli/__pycache__/cli.cpython-314.pyc b/mycli/__pycache__/cli.cpython-314.pyc index 027f203..701647e 100644 Binary files a/mycli/__pycache__/cli.cpython-314.pyc and b/mycli/__pycache__/cli.cpython-314.pyc differ diff --git a/mycli/cli.py b/mycli/cli.py index 8b3c61f..c95bf5e 100644 --- a/mycli/cli.py +++ b/mycli/cli.py @@ -2,7 +2,7 @@ import click -from mycli.commands import pdf, image, latex, video, bib, utils, env_cmd, ssh_cmd, git_cmd, self_cmd, config_cmd +from mycli.commands import pdf, image, latex, video, bib, utils, env_cmd, ssh_cmd, git_cmd, self_cmd, config_cmd, wizard_cmd @click.group() @@ -24,6 +24,7 @@ cli.add_command(ssh_cmd) cli.add_command(git_cmd) cli.add_command(self_cmd) cli.add_command(config_cmd) +cli.add_command(wizard_cmd) def main(): diff --git a/mycli/commands/__init__.py b/mycli/commands/__init__.py index 79bfce1..39e9eb7 100644 --- a/mycli/commands/__init__.py +++ b/mycli/commands/__init__.py @@ -6,5 +6,6 @@ from .ssh import ssh_cmd from .git import git_cmd from .self_mgmt import self_cmd from .config_cmd import config_cmd +from .config_wizard import wizard_cmd -__all__ = ["pdf", "image", "latex", "video", "bib", "utils", "env_cmd", "ssh_cmd", "git_cmd", "self_cmd", "config_cmd"] +__all__ = ["pdf", "image", "latex", "video", "bib", "utils", "env_cmd", "ssh_cmd", "git_cmd", "self_cmd", "config_cmd", "wizard_cmd"] diff --git a/mycli/commands/__pycache__/__init__.cpython-310.pyc b/mycli/commands/__pycache__/__init__.cpython-310.pyc index d08acca..4e0088d 100644 Binary files a/mycli/commands/__pycache__/__init__.cpython-310.pyc and b/mycli/commands/__pycache__/__init__.cpython-310.pyc differ diff --git a/mycli/commands/__pycache__/__init__.cpython-314.pyc b/mycli/commands/__pycache__/__init__.cpython-314.pyc index d6306df..704ff36 100644 Binary files a/mycli/commands/__pycache__/__init__.cpython-314.pyc and b/mycli/commands/__pycache__/__init__.cpython-314.pyc differ diff --git a/mycli/commands/__pycache__/config_cmd.cpython-314.pyc b/mycli/commands/__pycache__/config_cmd.cpython-314.pyc index 4b39f8c..3f5e739 100644 Binary files a/mycli/commands/__pycache__/config_cmd.cpython-314.pyc and b/mycli/commands/__pycache__/config_cmd.cpython-314.pyc differ diff --git a/mycli/commands/__pycache__/env.cpython-314.pyc b/mycli/commands/__pycache__/env.cpython-314.pyc index 735d24a..b69eba2 100644 Binary files a/mycli/commands/__pycache__/env.cpython-314.pyc and b/mycli/commands/__pycache__/env.cpython-314.pyc differ diff --git a/mycli/commands/config_wizard.py b/mycli/commands/config_wizard.py new file mode 100644 index 0000000..b3b2738 --- /dev/null +++ b/mycli/commands/config_wizard.py @@ -0,0 +1,324 @@ +"""Interactive configuration wizard for mycli.""" + +import json +from pathlib import Path + +import click + +from mycli.config import config + + +def _prompt_choice(options: list[str], prompt: str = "Select") -> str | None: + """Show numbered options and get user choice.""" + click.echo(f"\n{prompt}:") + for i, opt in enumerate(options, 1): + click.echo(f" {i}. {opt}") + click.echo(" 0. Cancel") + + while True: + choice = click.prompt("Enter number", type=int, default=0) + if choice == 0: + return None + if 1 <= choice <= len(options): + return options[choice - 1] + click.echo("Invalid choice, try again.") + + +def _prompt_confirm(message: str) -> bool: + """Prompt for yes/no confirmation.""" + return click.confirm(message, default=False) + + +@click.group(name="wizard", invoke_without_command=True) +@click.pass_context +def wizard_cmd(ctx): + """Interactive configuration wizard.""" + if ctx.invoked_subcommand is None: + # Show main wizard menu + show_main_menu() + + +def show_main_menu(): + """Show the main wizard menu.""" + click.clear() + click.secho("╔══════════════════════════════════════╗", fg="cyan") + click.secho("║ MyCLI Configuration Wizard ║", fg="cyan") + click.secho("╚══════════════════════════════════════╝", fg="cyan") + + options = { + "1": ("Manage Paths", manage_paths), + "2": ("Manage SSH Hosts", manage_ssh_hosts), + "3": ("Manage API Keys", manage_api_keys), + "4": ("Manage Feishu Apps", manage_feishu_apps), + "5": ("Manage Git Proxies", manage_git_proxies), + "0": ("Exit", lambda: click.echo("Goodbye!")), + } + + while True: + click.echo("\nMain Menu:") + for key, (label, _) in options.items(): + click.echo(f" [{key}] {label}") + + choice = click.prompt("\nSelect option", type=str, default="0") + + if choice in options: + _, func = options[choice] + func() + if choice == "0": + break + else: + click.echo("Invalid choice.") + + +def manage_paths(): + """Interactive path management.""" + click.secho("\n--- Path Management ---", fg="cyan") + + paths = config.get("paths", {}) + + # Show current paths + click.echo("\nCurrent paths:") + for name, path in sorted(paths.items()): + exists = "✓" if Path(path).expanduser().exists() else "✗" + click.echo(f" [{exists}] {name}: {path}") + + action = _prompt_choice(["Add new path", "Edit path", "Delete path", "Back"]) + + if action == "Add new path": + name = click.prompt("Path name (e.g., 'mynewproject')") + path = click.prompt("Path value (e.g., '~/Documents/project')") + _config_set(f"paths.{name}", path) + click.secho(f"Added path: {name} = {path}", fg="green") + + elif action == "Edit path": + if not paths: + click.echo("No paths to edit.") + return + name = _prompt_choice(list(paths.keys()), "Select path to edit") + if name: + new_path = click.prompt("New path value", default=paths[name]) + _config_set(f"paths.{name}", new_path) + click.secho(f"Updated path: {name}", fg="green") + + elif action == "Delete path": + if not paths: + click.echo("No paths to delete.") + return + name = _prompt_choice(list(paths.keys()), "Select path to delete") + if name and _prompt_confirm(f"Delete path '{name}'?"): + _config_delete(f"paths.{name}") + click.secho(f"Deleted path: {name}", fg="green") + + +def manage_ssh_hosts(): + """Interactive SSH host management.""" + click.secho("\n--- SSH Host Management ---", fg="cyan") + + hosts = config.get("ssh_hosts", {}) + + click.echo("\nCurrent SSH hosts:") + for name, host_config in sorted(hosts.items()): + click.echo(f" {name}: {host_config['host']}:{host_config['port']}") + + action = _prompt_choice(["Add new host", "Edit host", "Delete host", "Back"]) + + if action == "Add new host": + name = click.prompt("Host name (e.g., 'myserver')") + host = click.prompt("SSH connection (e.g., 'user@host')") + port = click.prompt("Port", type=int, default=22) + has_key = click.confirm("Use SSH key?", default=False) + + host_config = {"host": host, "port": port} + if has_key: + key_path = click.prompt("SSH key path (e.g., '~/.ssh/id_rsa')") + host_config["key"] = key_path + + _config_set(f"ssh_hosts.{name}", host_config) + click.secho(f"Added SSH host: {name}", fg="green") + + elif action == "Edit host": + if not hosts: + click.echo("No hosts to edit.") + return + name = _prompt_choice(list(hosts.keys()), "Select host to edit") + if name: + host_config = hosts[name] + host = click.prompt("Host", default=host_config["host"]) + port = click.prompt("Port", type=int, default=host_config["port"]) + + host_config["host"] = host + host_config["port"] = port + _config_set(f"ssh_hosts.{name}", host_config) + click.secho(f"Updated SSH host: {name}", fg="green") + + elif action == "Delete host": + if not hosts: + click.echo("No hosts to delete.") + return + name = _prompt_choice(list(hosts.keys()), "Select host to delete") + if name and _prompt_confirm(f"Delete host '{name}'?"): + _config_delete(f"ssh_hosts.{name}") + click.secho(f"Deleted SSH host: {name}", fg="green") + + +def manage_api_keys(): + """Interactive API key management.""" + click.secho("\n--- API Key Management ---", fg="cyan") + + api_keys = config.get("api_keys", {}) + + click.echo("\nCurrent API keys:") + for name in sorted(api_keys.keys()): + value = api_keys[name] + masked = value[:8] + "..." + value[-4:] if len(value) > 12 else "***" + click.echo(f" {name}: {masked}") + + action = _prompt_choice(["Add new API key", "Edit API key", "Delete API key", "Back"]) + + if action == "Add new API key": + name = click.prompt("API name (e.g., 'openai', 'anthropic')") + key = click.prompt("API key", hide_input=True) + _config_set(f"api_keys.{name}", key) + click.secho(f"Added API key: {name}", fg="green") + + elif action == "Edit API key": + if not api_keys: + click.echo("No API keys to edit.") + return + name = _prompt_choice(list(api_keys.keys()), "Select API key to edit") + if name: + key = click.prompt("New API key", hide_input=True) + _config_set(f"api_keys.{name}", key) + click.secho(f"Updated API key: {name}", fg="green") + + elif action == "Delete API key": + if not api_keys: + click.echo("No API keys to delete.") + return + name = _prompt_choice(list(api_keys.keys()), "Select API key to delete") + if name and _prompt_confirm(f"Delete API key '{name}'?"): + _config_delete(f"api_keys.{name}") + click.secho(f"Deleted API key: {name}", fg="green") + + +def manage_feishu_apps(): + """Interactive Feishu app management.""" + click.secho("\n--- Feishu App Management ---", fg="cyan") + + apps = config.get("feishu_apps", {}) + + click.echo("\nCurrent Feishu apps:") + for name in sorted(apps.keys()): + app = apps[name] + click.echo(f" {name}: {app['app_id']}") + + action = _prompt_choice(["Add new app", "Edit app", "Delete app", "Back"]) + + if action == "Add new app": + name = click.prompt("App name (e.g., 'mybot')") + app_id = click.prompt("App ID") + app_secret = click.prompt("App Secret", hide_input=True) + + app_config = {"app_id": app_id, "app_secret": app_secret} + _config_set(f"feishu_apps.{name}", app_config) + click.secho(f"Added Feishu app: {name}", fg="green") + + elif action == "Edit app": + if not apps: + click.echo("No apps to edit.") + return + name = _prompt_choice(list(apps.keys()), "Select app to edit") + if name: + app = apps[name] + app_id = click.prompt("App ID", default=app["app_id"]) + change_secret = click.confirm("Change app secret?", default=False) + + app["app_id"] = app_id + if change_secret: + app["app_secret"] = click.prompt("App Secret", hide_input=True) + + _config_set(f"feishu_apps.{name}", app) + click.secho(f"Updated Feishu app: {name}", fg="green") + + elif action == "Delete app": + if not apps: + click.echo("No apps to delete.") + return + name = _prompt_choice(list(apps.keys()), "Select app to delete") + if name and _prompt_confirm(f"Delete app '{name}'?"): + _config_delete(f"feishu_apps.{name}") + click.secho(f"Deleted Feishu app: {name}", fg="green") + + +def manage_git_proxies(): + """Interactive Git proxy management.""" + click.secho("\n--- Git Proxy Management ---", fg="cyan") + + proxies = config.get("git_proxies", {}) + + click.echo("\nCurrent Git proxies:") + for name, url in sorted(proxies.items()): + click.echo(f" {name}: {url}") + + action = _prompt_choice(["Add new proxy", "Edit proxy", "Delete proxy", "Back"]) + + if action == "Add new proxy": + name = click.prompt("Proxy name (e.g., 'fastgithub')") + url = click.prompt("Proxy URL (e.g., 'http://127.0.0.1:38457')") + _config_set(f"git_proxies.{name}", url) + click.secho(f"Added Git proxy: {name}", fg="green") + + elif action == "Edit proxy": + if not proxies: + click.echo("No proxies to edit.") + return + name = _prompt_choice(list(proxies.keys()), "Select proxy to edit") + if name: + url = click.prompt("Proxy URL", default=proxies[name]) + _config_set(f"git_proxies.{name}", url) + click.secho(f"Updated Git proxy: {name}", fg="green") + + elif action == "Delete proxy": + if not proxies: + click.echo("No proxies to delete.") + return + name = _prompt_choice(list(proxies.keys()), "Select proxy to delete") + if name and _prompt_confirm(f"Delete proxy '{name}'?"): + _config_delete(f"git_proxies.{name}") + click.secho(f"Deleted Git proxy: {name}", fg="green") + + +def _config_set(key: str, value): + """Set a config value directly in the JSON file.""" + config_path = config._config_path + with open(config_path) as f: + data = json.load(f) + + keys = key.split(".") + target = data + for k in keys[:-1]: + if k not in target: + target[k] = {} + target = target[k] + target[keys[-1]] = value + + with open(config_path, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + + +def _config_delete(key: str): + """Delete a config key directly from the JSON file.""" + config_path = config._config_path + with open(config_path) as f: + data = json.load(f) + + keys = key.split(".") + target = data + for k in keys[:-1]: + target = target[k] + del target[keys[-1]] + + with open(config_path, "w") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n")