- Add mycli wizard command for guided configuration - Interactive menus for paths, SSH, API keys, Feishu apps, git proxies - User-friendly prompts with numbered choices - Support add/edit/delete operations via wizard - Hide sensitive input (API keys, secrets) - Confirm before destructive operations
325 lines
12 KiB
Python
325 lines
12 KiB
Python
"""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")
|