diff --git a/.gitignore b/.gitignore index 8842f02..a961502 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ build/ .mypy_cache/ # Config contains sensitive data (API keys) -mycli/config.json +mytoolkit/config.json diff --git a/Makefile b/Makefile index 0034b5f..c2256e5 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,11 @@ ROOT_DIR := $(shell pwd) -VENV_MYCLI := $(ROOT_DIR)/.venv/bin/mycli -USER_LOCAL_MYCLI := $(HOME)/.local/bin/mycli +VENV_MYTOOLKIT := $(ROOT_DIR)/.venv/bin/mytoolkit +USER_LOCAL_MYTOOLKIT := $(HOME)/.local/bin/mytoolkit COMP_DIR := $(HOME)/.local/bin/completions # Use user's login shell for completion detection SHELL_NAME := $(notdir $(basename $(shell echo $$SHELL))) -.PHONY: help install uninstall _symlink-mycli _install-completions _uninstall-completions +.PHONY: help install uninstall _symlink-mytoolkit _install-completions _uninstall-completions help: @echo "Usage: make [target]" @@ -21,31 +21,31 @@ install: test -x .venv/bin/pip || python3 -m venv .venv; \ .venv/bin/pip install -e .; \ fi - @$(MAKE) _symlink-mycli + @$(MAKE) _symlink-mytoolkit @$(MAKE) _install-completions -_symlink-mycli: - @test -x "$(VENV_MYCLI)" || { echo "error: missing $(VENV_MYCLI)"; exit 1; } +_symlink-mytoolkit: + @test -x "$(VENV_MYTOOLKIT)" || { echo "error: missing $(VENV_MYTOOLKIT)"; exit 1; } @mkdir -p "$(HOME)/.local/bin" - @ln -sf "$(VENV_MYCLI)" "$(USER_LOCAL_MYCLI)" - @echo "Linked $(USER_LOCAL_MYCLI) -> $(VENV_MYCLI)" + @ln -sf "$(VENV_MYTOOLKIT)" "$(USER_LOCAL_MYTOOLKIT)" + @echo "Linked $(USER_LOCAL_MYTOOLKIT) -> $(VENV_MYTOOLKIT)" _install-completions: @mkdir -p "$(COMP_DIR)" ifeq ($(SHELL_NAME),zsh) - @_MYCLI_COMPLETE=zsh_source mycli > "$(COMP_DIR)/_mycli" 2>/dev/null && \ - echo "Installed zsh completion: $(COMP_DIR)/_mycli" || \ + @_MYTOOLKIT_COMPLETE=zsh_source "$(VENV_MYTOOLKIT)" > "$(COMP_DIR)/_mytoolkit" 2>/dev/null && \ + echo "Installed zsh completion: $(COMP_DIR)/_mytoolkit" || \ echo "Warning: failed to generate zsh completion" else ifeq ($(SHELL_NAME),bash) - @_MYCLI_COMPLETE=bash_source mycli > "$(COMP_DIR)/mycli.bash" 2>/dev/null && \ - echo "Installed bash completion: $(COMP_DIR)/mycli.bash" || \ + @_MYTOOLKIT_COMPLETE=bash_source "$(VENV_MYTOOLKIT)" > "$(COMP_DIR)/mytoolkit.bash" 2>/dev/null && \ + echo "Installed bash completion: $(COMP_DIR)/mytoolkit.bash" || \ echo "Warning: failed to generate bash completion" endif uninstall: _uninstall-completions - @rm -f "$(USER_LOCAL_MYCLI)" - @echo "Removed $(USER_LOCAL_MYCLI)" + @rm -f "$(USER_LOCAL_MYTOOLKIT)" + @echo "Removed $(USER_LOCAL_MYTOOLKIT)" _uninstall-completions: - @rm -f "$(COMP_DIR)/_mycli" "$(COMP_DIR)/mycli.bash" + @rm -f "$(COMP_DIR)/_mytoolkit" "$(COMP_DIR)/mytoolkit.bash" @echo "Removed completions" diff --git a/README.md b/README.md index d851dce..7ceb168 100644 --- a/README.md +++ b/README.md @@ -11,92 +11,93 @@ make install ## Commands ```bash -mycli --help +mytoolkit --help # PDF utilities -mycli pdf compress *.pdf -mycli pdf crop *.pdf -mycli pdf to-tiff *.pdf +mytoolkit pdf compress *.pdf +mytoolkit pdf crop *.pdf +mytoolkit pdf to-tiff *.pdf # Image utilities -mycli image eps-to-pdf *.eps -mycli image jpg-to-pdf *.jpg -o output.pdf -mycli image tiff-to-pdf *.tiff -mycli image tiff-compress *.tiff +mytoolkit image eps-to-pdf *.eps +mytoolkit image jpg-to-pdf *.jpg -o output.pdf +mytoolkit image tiff-to-pdf *.tiff +mytoolkit image tiff-compress *.tiff # LaTeX utilities -mycli latex compile *.tex -mycli latex count document.tex +mytoolkit latex compile *.tex +mytoolkit latex count document.tex # Video utilities -mycli video avi-to-mp4 *.avi +mytoolkit video avi-to-mp4 *.avi # BibTeX utilities -mycli bib to-markdown *.bib -mycli bib cv-update +mytoolkit bib to-markdown *.bib +mytoolkit bib cv-update # Utilities -mycli utils albany-cleanup +mytoolkit utils albany-cleanup # Environment & Navigation -mycli env list # List configured paths -mycli env goto research # Print path for cd -mycli env cd apaam # Start shell in path -mycli env status # Show environment status -cd $(mycli env goto study) # Use in scripts +mytoolkit env list # List configured paths +mytoolkit env goto research # Print path for cd +mytoolkit env cd apaam # Start shell in path +mytoolkit env status # Show environment status +cd $(mytoolkit env goto study) # Use in scripts # SSH Connections -mycli ssh list # List configured hosts -mycli ssh connect tianhe # SSH to host -mycli ssh connect starlight -mycli ssh tunnel tianhe # Create port tunnel -mycli ssh cmd tianhe "ls" # Run remote command +mytoolkit ssh list # List configured hosts +mytoolkit ssh connect tianhe # SSH to host +mytoolkit ssh connect starlight +mytoolkit ssh tunnel tianhe # Create port tunnel +mytoolkit ssh cmd tianhe "ls" # Run remote command # Git Proxy Management -mycli git proxy # Show proxy status -mycli git proxy fastgithub # Set proxy -mycli git proxy pandafan -mycli git proxy -u # Unset proxy -mycli git proxy-list # List available proxies +# Git Proxy Management +mytoolkit git proxy status # Show proxy status +mytoolkit git proxy set fastgithub # Set proxy +mytoolkit git proxy set pandafan +mytoolkit git proxy unset # Unset proxy +mytoolkit git proxy list # List available proxies # Self Management -mycli self update # Update mycli -mycli self uninstall # Uninstall mycli -mycli self info # Show installation info +mytoolkit self update # Update mytoolkit +mytoolkit self uninstall # Uninstall mytoolkit +mytoolkit self info # Show installation info # Configuration (config.json) - 增删改查 # 方式1: 交互式向导(推荐,无需记忆命令) -mycli wizard # 启动交互式配置向导 +mytoolkit wizard # 启动交互式配置向导 # 方式2: 命令行直接操作 -mycli config show # Show full config -mycli config list # List top-level sections -mycli config get paths # Get paths section -mycli config get paths.apaam # Get specific path -mycli config get api_keys.deepseek # Get API key -mycli config set paths.new ~/Documents/new # Set simple value -mycli config set api_keys.newkey abc123 # Set new API key -mycli config set --json ssh_hosts.new '{"host":"user@host","port":22}' -mycli config delete paths.old # Delete a key -mycli config delete api_keys.old -y # Delete without confirmation -mycli config paths # List all paths with existence check -mycli config where # Show config file location -mycli config edit # Edit config in $EDITOR -mycli config init # Create config from template +mytoolkit config show # Show full config +mytoolkit config list # List top-level sections +mytoolkit config get paths # Get paths section +mytoolkit config get paths.apaam # Get specific path +mytoolkit config get api_keys.deepseek # Get API key +mytoolkit config set paths.new ~/Documents/new # Set simple value +mytoolkit config set api_keys.newkey abc123 # Set new API key +mytoolkit config set --json ssh_hosts.new '{"host":"user@host","port":22}' +mytoolkit config delete paths.old # Delete a key +mytoolkit config delete api_keys.old -y # Delete without confirmation +mytoolkit config paths # List all paths with existence check +mytoolkit config where # Show config file location +mytoolkit config edit # Edit config in $EDITOR +mytoolkit config init # Create config from template # Environment Variables (API Keys) -mycli env status # Show paths, API keys, Feishu status -mycli env export # Print all export statements -mycli env export deepseek # Export specific API key -mycli env export qwen -mycli env export feishu # Export all Feishu apps -mycli env export claude # Export specific Feishu app -eval "$(mycli env export)" # Load all in current shell -mycli env init # Print shell init snippet +mytoolkit env status # Show paths, API keys, Feishu status +mytoolkit env export # Print all export statements +mytoolkit env export deepseek # Export specific API key +mytoolkit env export qwen +mytoolkit env export feishu # Export all Feishu apps +mytoolkit env export claude # Export specific Feishu app +eval "$(mytoolkit env export)" # Load all in current shell +mytoolkit env init # Print shell init snippet ``` ## Configuration -All settings are stored in `mycli/config.json`: +All settings are stored in `mytoolkit/config.json`: - `paths`: Quick navigation paths - `ssh_hosts`: SSH connection settings - `git_proxies`: Git proxy URLs @@ -113,17 +114,17 @@ The repository includes `config.json.example` as a template. 1. Copy the example config: ```bash - cp mycli/config.json.example mycli/config.json + cp mytoolkit/config.json.example mytoolkit/config.json ``` 2. Edit with your actual API keys: ```bash - mycli config edit + mytoolkit config edit ``` 3. Add to your `~/.zshrc` to auto-load: ```bash - eval "$(mycli env export 2>/dev/null)" + eval "$(mytoolkit env export 2>/dev/null)" ``` ## Uninstall diff --git a/mycli/__pycache__/__init__.cpython-310.pyc b/mycli/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index de2e24a..0000000 Binary files a/mycli/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/mycli/__pycache__/__init__.cpython-314.pyc b/mycli/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 0d956d7..0000000 Binary files a/mycli/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/mycli/__pycache__/cli.cpython-310.pyc b/mycli/__pycache__/cli.cpython-310.pyc deleted file mode 100644 index a540f68..0000000 Binary files a/mycli/__pycache__/cli.cpython-310.pyc and /dev/null differ diff --git a/mycli/__pycache__/cli.cpython-314.pyc b/mycli/__pycache__/cli.cpython-314.pyc deleted file mode 100644 index 701647e..0000000 Binary files a/mycli/__pycache__/cli.cpython-314.pyc and /dev/null differ diff --git a/mycli/__pycache__/config.cpython-310.pyc b/mycli/__pycache__/config.cpython-310.pyc deleted file mode 100644 index 93b9c17..0000000 Binary files a/mycli/__pycache__/config.cpython-310.pyc and /dev/null differ diff --git a/mycli/__pycache__/config.cpython-314.pyc b/mycli/__pycache__/config.cpython-314.pyc deleted file mode 100644 index 8a7bf93..0000000 Binary files a/mycli/__pycache__/config.cpython-314.pyc and /dev/null differ diff --git a/mycli/__pycache__/utils.cpython-310.pyc b/mycli/__pycache__/utils.cpython-310.pyc deleted file mode 100644 index 24dfad9..0000000 Binary files a/mycli/__pycache__/utils.cpython-310.pyc and /dev/null differ diff --git a/mycli/__pycache__/utils.cpython-314.pyc b/mycli/__pycache__/utils.cpython-314.pyc deleted file mode 100644 index dda4fdb..0000000 Binary files a/mycli/__pycache__/utils.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/__init__.cpython-310.pyc b/mycli/commands/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 4e0088d..0000000 Binary files a/mycli/commands/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/__init__.cpython-314.pyc b/mycli/commands/__pycache__/__init__.cpython-314.pyc deleted file mode 100644 index 704ff36..0000000 Binary files a/mycli/commands/__pycache__/__init__.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/bib.cpython-310.pyc b/mycli/commands/__pycache__/bib.cpython-310.pyc deleted file mode 100644 index f2a7edb..0000000 Binary files a/mycli/commands/__pycache__/bib.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/bib.cpython-314.pyc b/mycli/commands/__pycache__/bib.cpython-314.pyc deleted file mode 100644 index eae59f2..0000000 Binary files a/mycli/commands/__pycache__/bib.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/config_cmd.cpython-310.pyc b/mycli/commands/__pycache__/config_cmd.cpython-310.pyc deleted file mode 100644 index 44e291f..0000000 Binary files a/mycli/commands/__pycache__/config_cmd.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/config_cmd.cpython-314.pyc b/mycli/commands/__pycache__/config_cmd.cpython-314.pyc deleted file mode 100644 index 3f5e739..0000000 Binary files a/mycli/commands/__pycache__/config_cmd.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/env.cpython-310.pyc b/mycli/commands/__pycache__/env.cpython-310.pyc deleted file mode 100644 index 07069eb..0000000 Binary files a/mycli/commands/__pycache__/env.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/env.cpython-314.pyc b/mycli/commands/__pycache__/env.cpython-314.pyc deleted file mode 100644 index b69eba2..0000000 Binary files a/mycli/commands/__pycache__/env.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/git.cpython-310.pyc b/mycli/commands/__pycache__/git.cpython-310.pyc deleted file mode 100644 index 41f1ba2..0000000 Binary files a/mycli/commands/__pycache__/git.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/git.cpython-314.pyc b/mycli/commands/__pycache__/git.cpython-314.pyc deleted file mode 100644 index 6c0b5c1..0000000 Binary files a/mycli/commands/__pycache__/git.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/image.cpython-310.pyc b/mycli/commands/__pycache__/image.cpython-310.pyc deleted file mode 100644 index 57fbce9..0000000 Binary files a/mycli/commands/__pycache__/image.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/image.cpython-314.pyc b/mycli/commands/__pycache__/image.cpython-314.pyc deleted file mode 100644 index 6699881..0000000 Binary files a/mycli/commands/__pycache__/image.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/latex.cpython-310.pyc b/mycli/commands/__pycache__/latex.cpython-310.pyc deleted file mode 100644 index 1f830b8..0000000 Binary files a/mycli/commands/__pycache__/latex.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/latex.cpython-314.pyc b/mycli/commands/__pycache__/latex.cpython-314.pyc deleted file mode 100644 index 57408be..0000000 Binary files a/mycli/commands/__pycache__/latex.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/pdf.cpython-310.pyc b/mycli/commands/__pycache__/pdf.cpython-310.pyc deleted file mode 100644 index d239a8c..0000000 Binary files a/mycli/commands/__pycache__/pdf.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/pdf.cpython-314.pyc b/mycli/commands/__pycache__/pdf.cpython-314.pyc deleted file mode 100644 index cfd52cc..0000000 Binary files a/mycli/commands/__pycache__/pdf.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/self_mgmt.cpython-310.pyc b/mycli/commands/__pycache__/self_mgmt.cpython-310.pyc deleted file mode 100644 index dfe354e..0000000 Binary files a/mycli/commands/__pycache__/self_mgmt.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/self_mgmt.cpython-314.pyc b/mycli/commands/__pycache__/self_mgmt.cpython-314.pyc deleted file mode 100644 index 6fcdbdd..0000000 Binary files a/mycli/commands/__pycache__/self_mgmt.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/ssh.cpython-310.pyc b/mycli/commands/__pycache__/ssh.cpython-310.pyc deleted file mode 100644 index 42e3b83..0000000 Binary files a/mycli/commands/__pycache__/ssh.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/ssh.cpython-314.pyc b/mycli/commands/__pycache__/ssh.cpython-314.pyc deleted file mode 100644 index db94551..0000000 Binary files a/mycli/commands/__pycache__/ssh.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/utils.cpython-310.pyc b/mycli/commands/__pycache__/utils.cpython-310.pyc deleted file mode 100644 index 3139527..0000000 Binary files a/mycli/commands/__pycache__/utils.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/utils.cpython-314.pyc b/mycli/commands/__pycache__/utils.cpython-314.pyc deleted file mode 100644 index 4b0e84e..0000000 Binary files a/mycli/commands/__pycache__/utils.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/video.cpython-310.pyc b/mycli/commands/__pycache__/video.cpython-310.pyc deleted file mode 100644 index 1cf8c90..0000000 Binary files a/mycli/commands/__pycache__/video.cpython-310.pyc and /dev/null differ diff --git a/mycli/commands/__pycache__/video.cpython-314.pyc b/mycli/commands/__pycache__/video.cpython-314.pyc deleted file mode 100644 index 45e2fd7..0000000 Binary files a/mycli/commands/__pycache__/video.cpython-314.pyc and /dev/null differ diff --git a/mycli/commands/config_cmd.py b/mycli/commands/config_cmd.py deleted file mode 100644 index 199a4f1..0000000 --- a/mycli/commands/config_cmd.py +++ /dev/null @@ -1,198 +0,0 @@ -"""Configuration management commands.""" - -import json -import os -from pathlib import Path - -import click - -from mycli.config import config -from mycli.utils import handle_errors - - -@click.group(name="config") -def config_cmd(): - """Manage mycli configuration.""" - pass - - -@config_cmd.command("show") -def show(): - """Show current configuration.""" - config.show() - - -@config_cmd.command("get") -@click.argument("key") -def get_value(key): - """Get a configuration value by key (dot notation). - - Examples: - mycli config get paths.research - mycli config get ssh_hosts.tianhe.port - mycli config get api_keys.deepseek - """ - value = config.get(key) - if value is not None: - if isinstance(value, dict): - print(json.dumps(value, indent=2, ensure_ascii=False)) - else: - print(value) - else: - click.echo(f"Key not found: {key}", err=True) - raise click.Exit(1) - - -@config_cmd.command("set") -@click.argument("key") -@click.argument("value") -@click.option("--json", "is_json", is_flag=True, help="Parse value as JSON") -def set_value(key, value, is_json): - """Set a configuration value. - - Examples: - mycli config set paths.newpath ~/Documents/new - mycli config set api_keys.newkey abc123 - mycli config set --json ssh_hosts.newhost '{"host":"user@host","port":22}' - """ - # Load current config - config_path = config._config_path - with open(config_path) as f: - data = json.load(f) - - # Parse value - if is_json: - try: - parsed_value = json.loads(value) - except json.JSONDecodeError as e: - click.echo(f"Invalid JSON: {e}", err=True) - raise click.Exit(1) - else: - parsed_value = value - - # Navigate to nested key - keys = key.split(".") - target = data - for k in keys[:-1]: - if k not in target: - target[k] = {} - target = target[k] - - # Set value - target[keys[-1]] = parsed_value - - # Save - with open(config_path, "w") as f: - json.dump(data, f, indent=2, ensure_ascii=False) - f.write("\n") - - click.secho(f"Set {key} = {parsed_value}", fg="green") - - -@config_cmd.command("delete") -@click.argument("key") -@click.option("--yes", "-y", is_flag=True, help="Skip confirmation") -def delete_value(key, yes): - """Delete a configuration key. - - Examples: - mycli config delete api_keys.oldkey - mycli config delete paths.oldpath - """ - # Check if exists - if config.get(key) is None: - click.echo(f"Key not found: {key}", err=True) - raise click.Exit(1) - - if not yes: - click.confirm(f"Delete '{key}'?", abort=True) - - # Load current config - config_path = config._config_path - with open(config_path) as f: - data = json.load(f) - - # Navigate and delete - keys = key.split(".") - target = data - for k in keys[:-1]: - if k not in target: - click.echo(f"Key not found: {key}", err=True) - raise click.Exit(1) - target = target[k] - - if keys[-1] in target: - del target[keys[-1]] - else: - click.echo(f"Key not found: {key}", err=True) - raise click.Exit(1) - - # Save - with open(config_path, "w") as f: - json.dump(data, f, indent=2, ensure_ascii=False) - f.write("\n") - - click.secho(f"Deleted {key}", fg="green") - - -@config_cmd.command("list") -def list_keys(): - """List all top-level configuration keys.""" - config_path = config._config_path - with open(config_path) as f: - data = json.load(f) - - click.secho("Configuration sections:", fg="cyan") - for key in sorted(data.keys()): - value = data[key] - type_str = type(value).__name__ - if isinstance(value, dict): - count = len(value) - click.echo(f" {key:20} ({type_str}, {count} items)") - elif isinstance(value, list): - click.echo(f" {key:20} ({type_str}, {len(value)} items)") - else: - click.echo(f" {key:20} ({type_str})") - - -@config_cmd.command("paths") -def list_paths(): - """List all configured paths.""" - paths = config.get_all_paths() - max_len = max(len(name) for name in paths.keys()) - for name, path in sorted(paths.items()): - exists = "✓" if path.exists() else "✗" - click.echo(f"{name:{max_len}} [{exists}] {path}") - - -@config_cmd.command("edit") -def edit(): - """Open configuration file in editor.""" - config.edit() - - -@config_cmd.command("where") -def where(): - """Show configuration file location.""" - click.echo(config._config_path) - - -@config_cmd.command("init") -def init_config(): - """Create initial config from example template.""" - config_path = config._config_path - example_path = config_path.parent / "config.json.example" - - if config_path.exists(): - click.echo(f"Config already exists: {config_path}", err=True) - click.echo("Use 'mycli config edit' to modify it") - raise click.Exit(1) - - if not example_path.exists(): - click.echo(f"Example config not found: {example_path}", err=True) - raise click.Exit(1) - - import shutil - shutil.copy(example_path, config_path) - click.secho(f"Created config from template: {config_path}", fg="green") - click.echo("Edit it to add your API keys and customize paths") diff --git a/mycli/commands/config_wizard.py b/mycli/commands/config_wizard.py deleted file mode 100644 index b3b2738..0000000 --- a/mycli/commands/config_wizard.py +++ /dev/null @@ -1,324 +0,0 @@ -"""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") diff --git a/mycli/commands/env.py b/mycli/commands/env.py deleted file mode 100644 index 79c3f00..0000000 --- a/mycli/commands/env.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Environment paths and quick navigation.""" - -import functools -import os -import subprocess -from pathlib import Path - -import click - -from mycli.config import config -from mycli.utils import handle_errors - - -@functools.lru_cache(maxsize=1) -def _get_paths() -> dict[str, Path]: - """Lazy load paths from config.""" - return config.get_all_paths() - - -@click.group(name="env") -def env_cmd(): - """Environment paths and navigation.""" - pass - - -@env_cmd.command("goto") -@click.argument("name") -@handle_errors -def goto(name): - """Print path for cd (use: cd $(mycli env goto )).""" - paths = _get_paths() - if name not in paths: - click.echo(f"Unknown path: {name}", err=True) - click.echo(f"Available: {', '.join(paths.keys())}", err=True) - raise click.Exit(1) - click.echo(paths[name]) - - -@env_cmd.command("list") -@handle_errors -def list_paths(): - """List all configured paths.""" - paths = _get_paths() - max_len = max(len(k) for k in paths.keys()) - for name, path in sorted(paths.items()): - exists = "✓" if path.exists() else "✗" - click.echo(f"{name:{max_len}} {exists} {path}") - - -@env_cmd.command("cd") -@click.argument("name") -@handle_errors -def cd_path(name): - """Change directory (launches new shell).""" - paths = _get_paths() - if name not in paths: - click.echo(f"Unknown path: {name}", err=True) - raise click.Exit(1) - - target = paths[name] - if not target.exists(): - click.echo(f"Path does not exist: {target}", err=True) - raise click.Exit(1) - - shell = os.environ.get("SHELL", "/bin/zsh") - click.echo(f"Starting shell in: {target}") - subprocess.run([shell], cwd=target) - - -@env_cmd.command("status") -@handle_errors -def status(): - """Show environment status.""" - paths = _get_paths() - click.secho("=== Paths ===", fg="cyan") - for name, path in sorted(paths.items()): - exists = "✓" if path.exists() else "✗" - click.echo(f" {name:12} [{exists}] {path}") - - click.secho("\n=== OpenFOAM ===", fg="cyan") - openfoam_path = "/Volumes/OpenFOAM/openfoam/build" - if Path("/Volumes/OpenFOAM").exists(): - click.secho(f" Mounted: {openfoam_path}", fg="green") - else: - click.secho(" Not mounted", fg="red") - - # API Keys status (without showing actual values) - click.secho("\n=== API Keys ===", fg="cyan") - api_keys = config.get("api_keys", {}) - for name in api_keys.keys(): - env_var = f"{name.upper()}_API_KEY" - is_set = "✓" if os.environ.get(env_var) else "✗" - click.echo(f" {name:12} [{is_set}] {env_var}") - - # Feishu Apps status - click.secho("\n=== Feishu Apps ===", fg="cyan") - feishu_apps = config.get("feishu_apps", {}) - for app_name in feishu_apps.keys(): - app_id_var = f"{app_name.upper()}_APP_ID" - is_set = "✓" if os.environ.get(app_id_var) else "✗" - click.echo(f" {app_name:12} [{is_set}] {app_id_var}") - - -@env_cmd.command("export") -@click.argument("name", required=False) -def export_env(name): - """Print export statements for environment variables. - - Examples: - mycli env export # Export all - mycli env export deepseek # Export specific API key - mycli env export feishu # Export all Feishu apps - eval "$(mycli env export)" # Load in current shell - """ - if name is None: - # Export all - _export_api_keys() - _export_feishu_apps() - elif name == "feishu": - _export_feishu_apps() - elif name in config.get("api_keys", {}): - _export_api_key(name) - elif name in config.get("feishu_apps", {}): - _export_feishu_app(name) - else: - click.echo(f"Unknown export target: {name}", err=True) - click.echo(f"Available: api_keys ({', '.join(config.get('api_keys', {}).keys())}), " - f"feishu_apps ({', '.join(config.get('feishu_apps', {}).keys())})", err=True) - raise click.Exit(1) - - -def _export_api_key(name: str): - """Export a single API key.""" - api_keys = config.get("api_keys", {}) - if name not in api_keys: - return - env_var = f"{name.upper()}_API_KEY" - value = api_keys[name] - click.echo(f"export {env_var}={value}") - - -def _export_api_keys(): - """Export all API keys.""" - api_keys = config.get("api_keys", {}) - for name in api_keys.keys(): - _export_api_key(name) - - -def _export_feishu_app(name: str): - """Export a single Feishu app.""" - feishu_apps = config.get("feishu_apps", {}) - if name not in feishu_apps: - return - app = feishu_apps[name] - prefix = name.upper() - click.echo(f"export {prefix}_APP_ID={app['app_id']}") - click.echo(f"export {prefix}_APP_SECRET={app['app_secret']}") - - -def _export_feishu_apps(): - """Export all Feishu apps.""" - feishu_apps = config.get("feishu_apps", {}) - for name in feishu_apps.keys(): - _export_feishu_app(name) - - -@env_cmd.command("init") -@click.option("--shell", "-s", "shell_type", type=click.Choice(["bash", "zsh", "fish"]), default="zsh") -def init_shell(shell_type): - """Print shell initialization snippet. - - Add this to your ~/.zshrc or ~/.bashrc: - eval "$(mycli env init)" - """ - snippet = f"""# MyCLI environment initialization -# Add this line to your ~/.{shell_type}rc: -# eval "$(mycli env init)" - -# Export API keys and app credentials -eval "$(mycli env export 2>/dev/null)" -""" - click.echo(snippet) diff --git a/mycli/commands/git.py b/mycli/commands/git.py deleted file mode 100644 index fd3bd8b..0000000 --- a/mycli/commands/git.py +++ /dev/null @@ -1,98 +0,0 @@ -"""Git utilities including proxy management.""" - -import subprocess - -import click - -from mycli.config import config - - -def _get_proxies(): - """Get git proxies from config.""" - return config.get_all_git_proxies() - - -@click.group(name="git") -def git_cmd(): - """Git utilities.""" - pass - - -@git_cmd.command("proxy") -@click.argument("name", required=False) -@click.option("--unset", "-u", is_flag=True, help="Unset proxy") -@click.option("--status", "-s", is_flag=True, help="Show current proxy status") -def proxy_cmd(name, unset, status): - """Manage git proxy settings. - - Examples: - mycli git proxy fastgithub # Set fastgithub proxy - mycli git proxy pandafan # Set pandafan proxy - mycli git proxy -u # Unset proxy - mycli git proxy -s # Show status - """ - if status or (not name and not unset): - _show_status() - return - - if unset: - subprocess.run(["git", "config", "--global", "--unset", "http.proxy"], capture_output=True) - subprocess.run(["git", "config", "--global", "--unset", "https.proxy"], capture_output=True) - click.secho("Git proxy unset", fg="green") - return - - proxies = _get_proxies() - if name not in proxies: - click.echo(f"Unknown proxy: {name}", err=True) - click.echo(f"Available: {', '.join(proxies.keys())}", err=True) - raise click.Exit(1) - - proxy_url = proxies[name] - subprocess.run(["git", "config", "--global", "http.proxy", proxy_url], check=True) - subprocess.run(["git", "config", "--global", "https.proxy", proxy_url], check=True) - click.secho(f"Git proxy set to {name}: {proxy_url}", fg="green") - - -def _show_status(): - """Show current git proxy status.""" - result = subprocess.run( - ["git", "config", "--global", "http.proxy"], - capture_output=True, - text=True, - ) - http_proxy = result.stdout.strip() if result.returncode == 0 else "(not set)" - - result = subprocess.run( - ["git", "config", "--global", "https.proxy"], - capture_output=True, - text=True, - ) - https_proxy = result.stdout.strip() if result.returncode == 0 else "(not set)" - - # 检查当前设置的代理名称 - proxies = _get_proxies() - current_name = None - for proxy_name, url in proxies.items(): - if http_proxy == url: - current_name = proxy_name - break - - click.secho("Git proxy status:", fg="cyan") - if current_name: - click.echo(f" Active: {current_name}") - click.echo(f" http.proxy: {http_proxy}") - click.echo(f" https.proxy: {https_proxy}") - - click.secho("\nAvailable proxies:", fg="cyan") - for proxy_name, url in proxies.items(): - marker = "*" if proxy_name == current_name else " " - click.echo(f" [{marker}] {proxy_name:12} {url}") - - -@git_cmd.command("proxy-list") -def proxy_list(): - """List available git proxies.""" - proxies = _get_proxies() - click.secho("Available proxies:", fg="cyan") - for name, url in proxies.items(): - click.echo(f" {name:12} {url}") diff --git a/mycli/commands/self_mgmt.py b/mycli/commands/self_mgmt.py deleted file mode 100644 index a42c8e4..0000000 --- a/mycli/commands/self_mgmt.py +++ /dev/null @@ -1,86 +0,0 @@ -"""Self-management commands for mycli.""" - -import os -import subprocess -import sys -from pathlib import Path - -import click - - -def _get_project_root() -> Path: - """Get mycli project root.""" - # This file is at mycli/commands/self.py - return Path(__file__).parent.parent.parent - - -@click.group(name="self") -def self_cmd(): - """Manage mycli itself.""" - pass - - -@self_cmd.command("update") -def update(): - """Update mycli from the local repository.""" - root = _get_project_root() - - click.secho("Updating mycli...", fg="cyan") - - # Run make install - result = subprocess.run( - ["make", "install"], - cwd=str(root), - capture_output=True, - text=True, - ) - - if result.returncode == 0: - click.secho("mycli updated successfully.", fg="green") - else: - click.secho("Update failed:", fg="red", err=True) - click.echo(result.stderr, err=True) - raise click.Exit(1) - - -@self_cmd.command("uninstall") -def uninstall(): - """Uninstall mycli.""" - root = _get_project_root() - - click.secho("Uninstalling mycli...", fg="cyan") - - result = subprocess.run( - ["make", "uninstall"], - cwd=str(root), - capture_output=True, - text=True, - ) - - if result.returncode == 0: - click.secho("mycli uninstalled.", fg="green") - else: - click.secho("Uninstall failed:", fg="red", err=True) - click.echo(result.stderr, err=True) - raise click.Exit(1) - - -@self_cmd.command("info") -def info(): - """Show mycli installation info.""" - root = _get_project_root() - - click.secho("MyCLI Info:", fg="cyan") - click.echo(f" Project root: {root}") - click.echo(f" Commands dir: {Path(__file__).parent}") - - # Check if in PATH - mycli_path = subprocess.run( - ["which", "mycli"], - capture_output=True, - text=True, - ) - if mycli_path.returncode == 0: - click.echo(f" Executable: {mycli_path.stdout.strip()}") - else: - click.secho(" Executable: not in PATH", fg="yellow") diff --git a/mycli/config.py b/mycli/config.py deleted file mode 100644 index 793a2b3..0000000 --- a/mycli/config.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Configuration management for mycli.""" - -import json -import os -from pathlib import Path -from typing import Any - - -class Config: - """Configuration manager.""" - - def __init__(self): - self._data: dict = {} - self._config_path = Path(__file__).parent / "config.json" - self._load() - - def _load(self): - """Load configuration from JSON file.""" - if self._config_path.exists(): - with open(self._config_path) as f: - self._data = json.load(f) - else: - self._data = {} - - def get(self, key: str, default: Any = None) -> Any: - """Get configuration value by dot notation key. - - Examples: - config.get("paths.research") - config.get("ssh_hosts.tianhe.port") - """ - keys = key.split(".") - value = self._data - for k in keys: - if isinstance(value, dict) and k in value: - value = value[k] - else: - return default - return value - - def get_path(self, name: str) -> Path | None: - """Get a path and expand ~ to home directory.""" - path_str = self.get(f"paths.{name}") - if path_str: - return Path(path_str).expanduser() - return None - - def get_all_paths(self) -> dict[str, Path]: - """Get all paths as Path objects.""" - paths = self.get("paths", {}) - return {name: Path(path).expanduser() for name, path in paths.items()} - - def get_ssh_host(self, name: str) -> dict | None: - """Get SSH host configuration.""" - return self.get(f"ssh_hosts.{name}") - - def get_git_proxy(self, name: str) -> str | None: - """Get Git proxy URL.""" - return self.get(f"git_proxies.{name}") - - def get_all_git_proxies(self) -> dict[str, str]: - """Get all Git proxy configurations.""" - return self.get("git_proxies", {}) - - def edit(self): - """Open config file in default editor.""" - editor = os.environ.get("EDITOR", "vim") - os.system(f"{editor} {self._config_path}") - - def show(self): - """Display configuration.""" - import json - print(json.dumps(self._data, indent=2, ensure_ascii=False)) - - -# Global config instance -config = Config() diff --git a/mycli/__init__.py b/mytoolkit/__init__.py similarity index 100% rename from mycli/__init__.py rename to mytoolkit/__init__.py diff --git a/mycli/cli.py b/mytoolkit/cli.py similarity index 61% rename from mycli/cli.py rename to mytoolkit/cli.py index c95bf5e..453572a 100644 --- a/mycli/cli.py +++ b/mytoolkit/cli.py @@ -2,11 +2,12 @@ import click -from mycli.commands import pdf, image, latex, video, bib, utils, env_cmd, ssh_cmd, git_cmd, self_cmd, config_cmd, wizard_cmd +from mytoolkit.commands import pdf, image, latex, video, bib, utils, env_cmd, ssh_cmd, git_cmd +from mytoolkit.commands.self_mgmt import update_cmd, uninstall_cmd, self_cmd @click.group() -@click.version_option(version="0.1.0", prog_name="mycli") +@click.version_option(version="0.1.0", prog_name="mytoolkit") def cli(): """Personal CLI toolkit.""" pass @@ -22,9 +23,9 @@ cli.add_command(utils.utils) cli.add_command(env_cmd) cli.add_command(ssh_cmd) cli.add_command(git_cmd) +cli.add_command(update_cmd) +cli.add_command(uninstall_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/mytoolkit/commands/__init__.py similarity index 55% rename from mycli/commands/__init__.py rename to mytoolkit/commands/__init__.py index 39e9eb7..ff87eda 100644 --- a/mycli/commands/__init__.py +++ b/mytoolkit/commands/__init__.py @@ -4,8 +4,6 @@ from . import pdf, image, latex, video, bib, utils from .env import env_cmd 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 +from .self_mgmt import self_cmd, update_cmd, uninstall_cmd -__all__ = ["pdf", "image", "latex", "video", "bib", "utils", "env_cmd", "ssh_cmd", "git_cmd", "self_cmd", "config_cmd", "wizard_cmd"] +__all__ = ["pdf", "image", "latex", "video", "bib", "utils", "env_cmd", "ssh_cmd", "git_cmd", "self_cmd", "update_cmd", "uninstall_cmd"] diff --git a/mycli/commands/bib.py b/mytoolkit/commands/bib.py similarity index 87% rename from mycli/commands/bib.py rename to mytoolkit/commands/bib.py index be0851a..aa465b3 100644 --- a/mycli/commands/bib.py +++ b/mytoolkit/commands/bib.py @@ -5,8 +5,8 @@ from pathlib import Path import click -from mycli.config import config -from mycli.utils import handle_errors, run_command +from mytoolkit.config import config +from mytoolkit.utils import handle_errors, run_command @click.group() @@ -41,8 +41,10 @@ def to_markdown(files, dry_run): @handle_errors def cv_update(): """Update CV bibliography using biber.""" - cv_path = config.get_path("cv") - if not cv_path: + cv_path_str = config.get("path_cv") + if cv_path_str: + cv_path = Path(cv_path_str).expanduser() + else: # Fallback to hardcoded path cv_path = Path.home() / "Documents/Personal/resume_cv/cv_lai_maintaining_latex/cv_lai_maintaining" diff --git a/mytoolkit/commands/env.py b/mytoolkit/commands/env.py new file mode 100644 index 0000000..d1a3629 --- /dev/null +++ b/mytoolkit/commands/env.py @@ -0,0 +1,65 @@ +"""Environment variables management.""" + +import click + +from mytoolkit.config import config + + +@click.group(name="env") +def env_cmd(): + """Manage environment variables.""" + pass + + +@env_cmd.command("list") +def env_list(): + """List all vars.""" + vars = config.get_all() + if not vars: + click.echo("No vars configured.") + return + for name, value in sorted(vars.items()): + masked = "***" if any(x in name.lower() for x in ["key", "secret", "token", "pass"]) else value + click.echo(f"{name:20} = {masked}") + + +@env_cmd.command("get") +@click.argument("name") +def env_get(name): + """Get a var value.""" + value = config.get(name) + if value is None: + click.echo(f"Var not found: {name}", err=True) + raise click.Exit(1) + click.echo(value) + + +@env_cmd.command("set") +@click.argument("name") +@click.argument("value", required=False) +@click.option("--secret", is_flag=True, help="Hide input for secrets") +def env_set(name, value, secret): + """Set a var. Prompts if value not provided.""" + if value is None: + value = click.prompt("Value", hide_input=secret) + config.set(name, value) + click.secho(f"Set: {name}", fg="green") + + +@env_cmd.command("remove") +@click.argument("name") +@click.confirmation_option(prompt="Remove this var?") +def env_remove(name): + """Remove a var.""" + if config.remove(name): + click.secho(f"Removed: {name}", fg="green") + else: + click.echo(f"Var not found: {name}", err=True) + raise click.Exit(1) + + +@env_cmd.command("export") +def env_export(): + """Print export statements for shell eval.""" + for name, value in config.export().items(): + click.echo(f"export {name}={value}") diff --git a/mytoolkit/commands/git.py b/mytoolkit/commands/git.py new file mode 100644 index 0000000..acd9bb1 --- /dev/null +++ b/mytoolkit/commands/git.py @@ -0,0 +1,116 @@ +"""Git utilities including proxy management.""" + +import subprocess + +import click + +from mytoolkit.config import config + + +def _get_proxies() -> dict[str, str]: + """Get git proxies from config.""" + proxies = {} + for key, value in config.get_all().items(): + if key.startswith("proxy_"): + name = key[6:] # Remove 'proxy_' prefix + proxies[name] = value + return proxies + + +def _get_git_proxy(key: str) -> str: + """Get a global git proxy config value.""" + result = subprocess.run( + ["git", "config", "--global", key], + capture_output=True, + text=True, + check=False, + ) + return result.stdout.strip() if result.returncode == 0 else "(not set)" + + +def _unset_git_proxy(key: str) -> None: + """Unset a global git proxy config value (no-op if not set).""" + subprocess.run( + ["git", "config", "--global", "--unset", key], + capture_output=True, + check=False, + ) + + +@click.group(name="git") +def git_cmd() -> None: + """Git utilities.""" + pass + + +@git_cmd.group(name="proxy") +def proxy_cmd() -> None: + """Manage git proxy settings.""" + pass + + +def _complete_proxy_names(_ctx, _param, incomplete): + """Shell completion for proxy names.""" + return [ + name for name in _get_proxies() if name.startswith(incomplete) + ] + + +@proxy_cmd.command("set") +@click.argument("name", shell_complete=_complete_proxy_names) +def proxy_set(name: str) -> None: + """Set git proxy by name.""" + proxies = _get_proxies() + if name not in proxies: + click.echo(f"Unknown proxy: {name}", err=True) + click.echo(f"Available: {', '.join(proxies.keys())}", err=True) + raise click.Exit(1) + + proxy_url = proxies[name] + subprocess.run( + ["git", "config", "--global", "http.proxy", proxy_url], check=True + ) + subprocess.run( + ["git", "config", "--global", "https.proxy", proxy_url], check=True + ) + click.secho(f"Git proxy set to {name}: {proxy_url}", fg="green") + + +@proxy_cmd.command("unset") +def proxy_unset() -> None: + """Unset git proxy.""" + _unset_git_proxy("http.proxy") + _unset_git_proxy("https.proxy") + click.secho("Git proxy unset", fg="green") + + +@proxy_cmd.command("status") +def proxy_status() -> None: + """Show current git proxy status.""" + http_proxy = _get_git_proxy("http.proxy") + https_proxy = _get_git_proxy("https.proxy") + + proxies = _get_proxies() + current_name = next( + (p for p, url in proxies.items() if url == http_proxy), None + ) + + click.secho("Git proxy status:", fg="cyan") + if current_name: + click.echo(f" Active: {current_name}") + click.echo(f" http.proxy: {http_proxy}") + click.echo(f" https.proxy: {https_proxy}") + + click.secho("\nAvailable proxies:", fg="cyan") + for proxy_name, url in proxies.items(): + marker = "*" if proxy_name == current_name else " " + click.echo(f" [{marker}] {proxy_name:12} {url}") + + +@proxy_cmd.command("list") +def proxy_list_cmd() -> None: + """List available git proxies.""" + proxies = _get_proxies() + click.secho("Available proxies:", fg="cyan") + for name, url in proxies.items(): + click.echo(f" {name:12} {url}") diff --git a/mycli/commands/image.py b/mytoolkit/commands/image.py similarity index 98% rename from mycli/commands/image.py rename to mytoolkit/commands/image.py index 2b5ac30..175e1ca 100644 --- a/mycli/commands/image.py +++ b/mytoolkit/commands/image.py @@ -5,7 +5,7 @@ from pathlib import Path import click -from mycli.utils import handle_errors, run_command +from mytoolkit.utils import handle_errors, run_command @click.group() diff --git a/mycli/commands/latex.py b/mytoolkit/commands/latex.py similarity index 95% rename from mycli/commands/latex.py rename to mytoolkit/commands/latex.py index e2cec03..6710d33 100644 --- a/mycli/commands/latex.py +++ b/mytoolkit/commands/latex.py @@ -5,7 +5,7 @@ from pathlib import Path import click -from mycli.utils import handle_errors, run_command +from mytoolkit.utils import handle_errors, run_command @click.group() diff --git a/mycli/commands/pdf.py b/mytoolkit/commands/pdf.py similarity index 98% rename from mycli/commands/pdf.py rename to mytoolkit/commands/pdf.py index 99e14be..3d39d60 100644 --- a/mycli/commands/pdf.py +++ b/mytoolkit/commands/pdf.py @@ -5,7 +5,7 @@ from pathlib import Path import click -from mycli.utils import handle_errors, run_command +from mytoolkit.utils import handle_errors, run_command @click.group() diff --git a/mytoolkit/commands/self_mgmt.py b/mytoolkit/commands/self_mgmt.py new file mode 100644 index 0000000..628b8d6 --- /dev/null +++ b/mytoolkit/commands/self_mgmt.py @@ -0,0 +1,69 @@ +"""Self-management commands for mytoolkit.""" + +import subprocess +from pathlib import Path + +import click + + +def _get_project_root() -> Path: + """Get mytoolkit project root.""" + # This file is at mytoolkit/commands/self_mgmt.py + return Path(__file__).parent.parent.parent + + +@click.command(name="update") +def update_cmd(): + """Update mytoolkit from the local repository.""" + root = _get_project_root() + + click.secho("Updating mytoolkit...", fg="cyan") + + # Run make install + result = subprocess.run( + ["make", "install"], + cwd=str(root), + capture_output=True, + text=True, + ) + + if result.returncode == 0: + click.secho("mytoolkit updated successfully.", fg="green") + else: + click.secho("Update failed:", fg="red", err=True) + click.echo(result.stderr, err=True) + raise click.Exit(1) + + +@click.command(name="uninstall") +def uninstall_cmd(): + """Uninstall mytoolkit.""" + root = _get_project_root() + + click.secho("Uninstalling mytoolkit...", fg="cyan") + + result = subprocess.run( + ["make", "uninstall"], + cwd=str(root), + capture_output=True, + text=True, + ) + + if result.returncode == 0: + click.secho("mytoolkit uninstalled.", fg="green") + else: + click.secho("Uninstall failed:", fg="red", err=True) + click.echo(result.stderr, err=True) + raise click.Exit(1) + + +# Backward compatibility: keep self_cmd group for any existing scripts +@click.group(name="self") +def self_cmd(): + """Manage mytoolkit itself (legacy, use 'update'/'uninstall' directly).""" + pass + + +# Register commands in the group for backward compatibility +self_cmd.add_command(update_cmd, name="update") +self_cmd.add_command(uninstall_cmd, name="uninstall") diff --git a/mycli/commands/ssh.py b/mytoolkit/commands/ssh.py similarity index 80% rename from mycli/commands/ssh.py rename to mytoolkit/commands/ssh.py index 8dd2378..ff19cf0 100644 --- a/mycli/commands/ssh.py +++ b/mytoolkit/commands/ssh.py @@ -5,13 +5,24 @@ from pathlib import Path import click -from mycli.config import config -from mycli.utils import handle_errors +from mytoolkit.config import config +from mytoolkit.utils import handle_errors def _get_hosts() -> dict: """Get SSH hosts from config.""" - return config.get("ssh_hosts", {}) + hosts = {} + for key in config.get_all().keys(): + if key.startswith("ssh_") and not key.endswith("_port") and not key.endswith("_key"): + name = key[4:] # Remove 'ssh_' prefix + host = config.get(f"ssh_{name}") + port = config.get(f"ssh_{name}_port") or "22" + key_file = config.get(f"ssh_{name}_key") + if host: + hosts[name] = {"host": host, "port": int(port)} + if key_file: + hosts[name]["key"] = key_file + return hosts def _build_ssh_base_cmd(host_config: dict) -> list[str]: diff --git a/mycli/commands/utils.py b/mytoolkit/commands/utils.py similarity index 94% rename from mycli/commands/utils.py rename to mytoolkit/commands/utils.py index f3e5bf6..445154f 100644 --- a/mycli/commands/utils.py +++ b/mytoolkit/commands/utils.py @@ -4,7 +4,7 @@ from pathlib import Path import click -from mycli.utils import handle_errors +from mytoolkit.utils import handle_errors @click.group() diff --git a/mycli/commands/video.py b/mytoolkit/commands/video.py similarity index 94% rename from mycli/commands/video.py rename to mytoolkit/commands/video.py index e9f8450..64766ef 100644 --- a/mycli/commands/video.py +++ b/mytoolkit/commands/video.py @@ -5,7 +5,7 @@ from pathlib import Path import click -from mycli.utils import handle_errors, run_command +from mytoolkit.utils import handle_errors, run_command @click.group() diff --git a/mycli/config.json.example b/mytoolkit/config.json.example similarity index 100% rename from mycli/config.json.example rename to mytoolkit/config.json.example diff --git a/mytoolkit/config.py b/mytoolkit/config.py new file mode 100644 index 0000000..6770186 --- /dev/null +++ b/mytoolkit/config.py @@ -0,0 +1,55 @@ +"""Configuration management for mytoolkit.""" + +import json +from pathlib import Path + + +class Config: + """Simple config manager for environment variables.""" + + def __init__(self): + self._config_path = Path(__file__).parent / "config.json" + self._data = self._load() + + def _load(self) -> dict: + if self._config_path.exists(): + with open(self._config_path) as f: + return json.load(f) + return {"vars": {}} + + def save(self): + """Save config to file.""" + with open(self._config_path, "w") as f: + json.dump(self._data, f, indent=2, ensure_ascii=False) + f.write("\n") + + def get(self, name: str) -> str | None: + """Get var by name.""" + return self._data.get("vars", {}).get(name) + + def get_all(self) -> dict[str, str]: + """Get all vars.""" + return self._data.get("vars", {}).copy() + + def set(self, name: str, value: str): + """Set a var.""" + if "vars" not in self._data: + self._data["vars"] = {} + self._data["vars"][name] = value + self.save() + + def remove(self, name: str) -> bool: + """Remove a var. Returns True if existed.""" + if name in self._data.get("vars", {}): + del self._data["vars"][name] + self.save() + return True + return False + + def export(self) -> dict[str, str]: + """Get all vars with env-compatible names.""" + return {f"MYCLI_{k.upper()}": v for k, v in self.get_all().items()} + + +# Global instance +config = Config() diff --git a/mycli/utils.py b/mytoolkit/utils.py similarity index 95% rename from mycli/utils.py rename to mytoolkit/utils.py index fc7022b..be7306c 100644 --- a/mycli/utils.py +++ b/mytoolkit/utils.py @@ -1,4 +1,4 @@ -"""Utility functions and decorators for mycli.""" +"""Utility functions and decorators for mytoolkit.""" import functools import subprocess diff --git a/pyproject.toml b/pyproject.toml index bc77df9..715bfdb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "mycli" +name = "mytoolkit" version = "0.1.0" description = "Personal CLI toolkit" requires-python = ">=3.9" @@ -8,7 +8,7 @@ dependencies = [ ] [project.scripts] -mycli = "mycli.cli:main" +mytoolkit = "mytoolkit.cli:main" [build-system] requires = ["hatchling"] diff --git a/uv.lock b/uv.lock index be2a933..18f0b58 100644 --- a/uv.lock +++ b/uv.lock @@ -46,7 +46,7 @@ wheels = [ ] [[package]] -name = "mycli" +name = "mytoolkit" version = "0.1.0" source = { editable = "." } dependencies = [