init
This commit is contained in:
+1
-1
@@ -13,4 +13,4 @@ build/
|
||||
.mypy_cache/
|
||||
|
||||
# Config contains sensitive data (API keys)
|
||||
mycli/config.json
|
||||
mytoolkit/config.json
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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 <name>))."""
|
||||
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)
|
||||
@@ -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}")
|
||||
@@ -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")
|
||||
@@ -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()
|
||||
@@ -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():
|
||||
@@ -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"]
|
||||
@@ -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"
|
||||
|
||||
@@ -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}")
|
||||
@@ -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}")
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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")
|
||||
@@ -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]:
|
||||
@@ -4,7 +4,7 @@ from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from mycli.utils import handle_errors
|
||||
from mytoolkit.utils import handle_errors
|
||||
|
||||
|
||||
@click.group()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Utility functions and decorators for mycli."""
|
||||
"""Utility functions and decorators for mytoolkit."""
|
||||
|
||||
import functools
|
||||
import subprocess
|
||||
+2
-2
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user