This commit is contained in:
Zhengshou Lai
2026-04-14 18:39:09 +08:00
parent 00c608a3bd
commit 3aeaf7cb64
59 changed files with 417 additions and 1064 deletions
+1 -1
View File
@@ -13,4 +13,4 @@ build/
.mypy_cache/
# Config contains sensitive data (API keys)
mycli/config.json
mytoolkit/config.json
+15 -15
View File
@@ -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"
+60 -59
View File
@@ -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.
-198
View File
@@ -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")
-324
View File
@@ -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")
-182
View File
@@ -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)
-98
View File
@@ -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}")
-86
View File
@@ -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")
-77
View File
@@ -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()
+5 -4
View File
@@ -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"
+65
View File
@@ -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}")
+116
View File
@@ -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()
+69
View File
@@ -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()
+55
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
"""Utility functions and decorators for mycli."""
"""Utility functions and decorators for mytoolkit."""
import functools
import subprocess
+2 -2
View File
@@ -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"]
Generated
+1 -1
View File
@@ -46,7 +46,7 @@ wheels = [
]
[[package]]
name = "mycli"
name = "mytoolkit"
version = "0.1.0"
source = { editable = "." }
dependencies = [