Initial commit: mycli toolkit with all commands
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
ROOT_DIR := $(shell pwd)
|
||||
VENV_MYCLI := $(ROOT_DIR)/.venv/bin/mycli
|
||||
USER_LOCAL_MYCLI := $(HOME)/.local/bin/mycli
|
||||
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
|
||||
|
||||
help:
|
||||
@echo "Usage: make [target]"
|
||||
@echo ""
|
||||
@echo " install Sync venv, symlink bin, install completions"
|
||||
@echo " uninstall Remove bin and completions"
|
||||
|
||||
install:
|
||||
@cd "$(ROOT_DIR)" && \
|
||||
if command -v uv >/dev/null 2>&1; then \
|
||||
uv sync; \
|
||||
else \
|
||||
test -x .venv/bin/pip || python3 -m venv .venv; \
|
||||
.venv/bin/pip install -e .; \
|
||||
fi
|
||||
@$(MAKE) _symlink-mycli
|
||||
@$(MAKE) _install-completions
|
||||
|
||||
_symlink-mycli:
|
||||
@test -x "$(VENV_MYCLI)" || { echo "error: missing $(VENV_MYCLI)"; exit 1; }
|
||||
@mkdir -p "$(HOME)/.local/bin"
|
||||
@ln -sf "$(VENV_MYCLI)" "$(USER_LOCAL_MYCLI)"
|
||||
@echo "Linked $(USER_LOCAL_MYCLI) -> $(VENV_MYCLI)"
|
||||
|
||||
_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" || \
|
||||
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" || \
|
||||
echo "Warning: failed to generate bash completion"
|
||||
endif
|
||||
|
||||
uninstall: _uninstall-completions
|
||||
@rm -f "$(USER_LOCAL_MYCLI)"
|
||||
@echo "Removed $(USER_LOCAL_MYCLI)"
|
||||
|
||||
_uninstall-completions:
|
||||
@rm -f "$(COMP_DIR)/_mycli" "$(COMP_DIR)/mycli.bash"
|
||||
@echo "Removed completions"
|
||||
@@ -0,0 +1,88 @@
|
||||
# MyCLI
|
||||
|
||||
Personal CLI toolkit - unified interface for various utility scripts.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
make install
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
mycli --help
|
||||
|
||||
# PDF utilities
|
||||
mycli pdf compress *.pdf
|
||||
mycli pdf crop *.pdf
|
||||
mycli 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
|
||||
|
||||
# LaTeX utilities
|
||||
mycli latex compile *.tex
|
||||
mycli latex count document.tex
|
||||
|
||||
# Video utilities
|
||||
mycli video avi-to-mp4 *.avi
|
||||
|
||||
# BibTeX utilities
|
||||
mycli bib to-markdown *.bib
|
||||
mycli bib cv-update
|
||||
|
||||
# Utilities
|
||||
mycli 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
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
# Self Management
|
||||
mycli self update # Update mycli
|
||||
mycli self uninstall # Uninstall mycli
|
||||
mycli self info # Show installation info
|
||||
|
||||
# Configuration (config.json)
|
||||
mycli config show # Show full config
|
||||
mycli config get paths # Get paths section
|
||||
mycli config get paths.apaam # Get specific path
|
||||
mycli config paths # List all paths with existence check
|
||||
mycli config where # Show config file location
|
||||
mycli config edit # Edit config in $EDITOR
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
All settings are stored in `mycli/config.json`:
|
||||
- `paths`: Quick navigation paths
|
||||
- `ssh_hosts`: SSH connection settings
|
||||
- `git_proxies`: Git proxy URLs
|
||||
- `openfoam`: OpenFOAM mount settings
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
make uninstall
|
||||
```
|
||||
@@ -0,0 +1,3 @@
|
||||
"""MyCLI - Personal CLI toolkit."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
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.
@@ -0,0 +1,34 @@
|
||||
"""MyCLI main entry point."""
|
||||
|
||||
import click
|
||||
|
||||
from mycli.commands import pdf, image, latex, video, bib, utils, env_cmd, ssh_cmd, git_cmd, self_cmd, config_cmd
|
||||
|
||||
|
||||
@click.group()
|
||||
@click.version_option(version="0.1.0", prog_name="mycli")
|
||||
def cli():
|
||||
"""Personal CLI toolkit."""
|
||||
pass
|
||||
|
||||
|
||||
# Register command groups
|
||||
cli.add_command(pdf.pdf)
|
||||
cli.add_command(image.image)
|
||||
cli.add_command(latex.latex)
|
||||
cli.add_command(video.video)
|
||||
cli.add_command(bib.bib)
|
||||
cli.add_command(utils.utils)
|
||||
cli.add_command(env_cmd)
|
||||
cli.add_command(ssh_cmd)
|
||||
cli.add_command(git_cmd)
|
||||
cli.add_command(self_cmd)
|
||||
cli.add_command(config_cmd)
|
||||
|
||||
|
||||
def main():
|
||||
cli()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,10 @@
|
||||
"""MyCLI subcommands."""
|
||||
|
||||
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
|
||||
|
||||
__all__ = ["pdf", "image", "latex", "video", "bib", "utils", "env_cmd", "ssh_cmd", "git_cmd", "self_cmd", "config_cmd"]
|
||||
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.
@@ -0,0 +1,54 @@
|
||||
"""BibTeX utilities."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from mycli.config import config
|
||||
from mycli.utils import handle_errors, run_command
|
||||
|
||||
|
||||
@click.group()
|
||||
def bib():
|
||||
"""BibTeX and bibliography commands."""
|
||||
pass
|
||||
|
||||
|
||||
@bib.command("to-markdown")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def to_markdown(files, dry_run):
|
||||
"""Convert BibTeX files to Markdown."""
|
||||
for pattern in files:
|
||||
for bib_file in Path(".").glob(pattern):
|
||||
if bib_file.suffix.lower() != ".bib":
|
||||
continue
|
||||
output = bib_file.with_suffix(".md")
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would convert: {bib_file} -> {output}")
|
||||
continue
|
||||
|
||||
click.echo(f"Converting: {bib_file} -> {output}")
|
||||
content = bib_file.read_text()
|
||||
md_content = f"# Bibliography\n\n```bibtex\n{content}\n```\n"
|
||||
output.write_text(md_content)
|
||||
|
||||
|
||||
@bib.command("cv-update")
|
||||
@handle_errors
|
||||
def cv_update():
|
||||
"""Update CV bibliography using biber."""
|
||||
cv_path = config.get_path("cv")
|
||||
if not cv_path:
|
||||
# Fallback to hardcoded path
|
||||
cv_path = Path.home() / "Documents/Personal/resume_cv/cv_lai_maintaining_latex/cv_lai_maintaining"
|
||||
|
||||
if not cv_path.exists():
|
||||
click.echo(f"CV file not found: {cv_path}")
|
||||
return
|
||||
|
||||
click.echo(f"Updating CV bibliography: {cv_path}")
|
||||
run_command(["biber", str(cv_path)], check=False)
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Configuration management commands."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
import click
|
||||
|
||||
from mycli.config import config
|
||||
|
||||
|
||||
@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)."""
|
||||
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("paths")
|
||||
def list_paths():
|
||||
"""List all configured paths."""
|
||||
paths = config.get_all_paths()
|
||||
max_len = max(len(name) for name in paths.keys())
|
||||
from pathlib import Path
|
||||
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)
|
||||
@@ -0,0 +1,85 @@
|
||||
"""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")
|
||||
@@ -0,0 +1,98 @@
|
||||
"""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}")
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Image utilities."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from mycli.utils import handle_errors, run_command
|
||||
|
||||
|
||||
@click.group()
|
||||
def image():
|
||||
"""Image manipulation commands."""
|
||||
pass
|
||||
|
||||
|
||||
@image.command("eps-to-pdf")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def eps_to_pdf(files, dry_run):
|
||||
"""Convert EPS files to PDF."""
|
||||
for pattern in files:
|
||||
for eps_file in Path(".").glob(pattern):
|
||||
if eps_file.suffix.lower() not in (".eps", ".ps"):
|
||||
continue
|
||||
output = eps_file.with_suffix(".pdf")
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would convert: {eps_file} -> {output}")
|
||||
continue
|
||||
|
||||
click.echo(f"Converting: {eps_file} -> {output}")
|
||||
run_command(["epstopdf", str(eps_file)], check=False)
|
||||
|
||||
|
||||
@image.command("eps-fix")
|
||||
@click.argument("file", type=click.Path(exists=True))
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def eps_fix(file, dry_run):
|
||||
"""Fix EPS files created by print command."""
|
||||
path = Path(file)
|
||||
content = path.read_text()
|
||||
|
||||
if "/f/fill" not in content:
|
||||
click.echo("File already fixed or not created by print command")
|
||||
return
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would fix: {file}")
|
||||
return
|
||||
|
||||
lines = content.split("\n")
|
||||
new_lines = []
|
||||
for line in lines:
|
||||
if "/f/fill" in line:
|
||||
line = line.replace("/f/fill", "")
|
||||
new_lines.append(line)
|
||||
|
||||
path.write_text("\n".join(new_lines))
|
||||
click.echo(f"Fixed: {file}")
|
||||
|
||||
|
||||
@image.command("jpg-to-pdf")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option("--output", "-o", help="Output PDF file")
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def jpg_to_pdf(files, output, dry_run):
|
||||
"""Convert JPG files to PDF."""
|
||||
images = []
|
||||
for pattern in files:
|
||||
for jpg_file in Path(".").glob(pattern):
|
||||
if jpg_file.suffix.lower() in (".jpg", ".jpeg"):
|
||||
images.append(str(jpg_file))
|
||||
|
||||
if not images:
|
||||
click.echo("No JPG files found")
|
||||
return
|
||||
|
||||
output = output or "output.pdf"
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would convert {len(images)} images to {output}")
|
||||
return
|
||||
|
||||
click.echo(f"Converting {len(images)} images to {output}")
|
||||
run_command(["convert"] + images + [output], check=False)
|
||||
|
||||
|
||||
@image.command("tiff-to-pdf")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def tiff_to_pdf(files, dry_run):
|
||||
"""Convert TIFF files to PDF."""
|
||||
for pattern in files:
|
||||
for tiff_file in Path(".").glob(pattern):
|
||||
if tiff_file.suffix.lower() not in (".tif", ".tiff"):
|
||||
continue
|
||||
output = tiff_file.with_suffix(".pdf")
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would convert: {tiff_file} -> {output}")
|
||||
continue
|
||||
|
||||
click.echo(f"Converting: {tiff_file} -> {output}")
|
||||
run_command(["convert", str(tiff_file), str(output)], check=False)
|
||||
|
||||
|
||||
@image.command("tiff-compress")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def tiff_compress(files, dry_run):
|
||||
"""Compress TIFF files."""
|
||||
for pattern in files:
|
||||
for tiff_file in Path(".").glob(pattern):
|
||||
if tiff_file.suffix.lower() not in (".tif", ".tiff"):
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would compress: {tiff_file}")
|
||||
continue
|
||||
|
||||
click.echo(f"Compressing: {tiff_file}")
|
||||
run_command(
|
||||
["convert", str(tiff_file), "-compress", "zip", str(tiff_file)],
|
||||
check=False,
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
"""LaTeX utilities."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from mycli.utils import handle_errors, run_command
|
||||
|
||||
|
||||
@click.group()
|
||||
def latex():
|
||||
"""LaTeX compilation and utilities."""
|
||||
pass
|
||||
|
||||
|
||||
@latex.command("compile")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option("--engine", "-e", default="pdflatex", help="LaTeX engine")
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def compile(files, engine, dry_run):
|
||||
"""Compile LaTeX files."""
|
||||
for pattern in files:
|
||||
for tex_file in Path(".").glob(pattern):
|
||||
if tex_file.suffix.lower() != ".tex":
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would compile: {tex_file}")
|
||||
continue
|
||||
|
||||
click.echo(f"Compiling: {tex_file}")
|
||||
run_command([engine, str(tex_file)], check=False)
|
||||
|
||||
|
||||
@latex.command("count")
|
||||
@click.argument("file", type=click.Path(exists=True))
|
||||
@handle_errors
|
||||
def count(file):
|
||||
"""Count words in a LaTeX file (excluding commands)."""
|
||||
result = run_command(
|
||||
["detex", file],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
words = len(result.stdout.split())
|
||||
click.echo(f"Words: {words}")
|
||||
@@ -0,0 +1,98 @@
|
||||
"""PDF utilities."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from mycli.utils import handle_errors, run_command
|
||||
|
||||
|
||||
@click.group()
|
||||
def pdf():
|
||||
"""PDF manipulation commands."""
|
||||
pass
|
||||
|
||||
|
||||
@pdf.command("compress")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option(
|
||||
"--quality",
|
||||
"-q",
|
||||
type=click.Choice(["screen", "ebook", "printer", "prepress"]),
|
||||
default="screen",
|
||||
help="Compression quality level",
|
||||
)
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def compress(files, quality, dry_run):
|
||||
"""Compress PDF files using ghostscript."""
|
||||
for pattern in files:
|
||||
for pdf_file in Path(".").glob(pattern):
|
||||
if pdf_file.suffix.lower() != ".pdf":
|
||||
continue
|
||||
|
||||
output = pdf_file.with_suffix(".compressed.pdf")
|
||||
cmd = [
|
||||
"gs",
|
||||
"-sDEVICE=pdfwrite",
|
||||
"-dNOPAUSE",
|
||||
"-dQUIET",
|
||||
"-dBATCH",
|
||||
f"-dPDFSETTINGS=/{quality}",
|
||||
"-dCompatibilityLevel=1.4",
|
||||
f"-sOutputFile={output}",
|
||||
str(pdf_file),
|
||||
]
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would compress: {pdf_file} -> {output}")
|
||||
continue
|
||||
|
||||
click.echo(f"Compressing: {pdf_file}")
|
||||
run_command(cmd, check=True)
|
||||
output.replace(pdf_file)
|
||||
click.echo(f" Done: {pdf_file}")
|
||||
|
||||
|
||||
@pdf.command("crop")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def crop(files, dry_run):
|
||||
"""Crop PDF files."""
|
||||
for pattern in files:
|
||||
for pdf_file in Path(".").glob(pattern):
|
||||
if pdf_file.suffix.lower() != ".pdf":
|
||||
continue
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would crop: {pdf_file}")
|
||||
continue
|
||||
|
||||
click.echo(f"Cropping: {pdf_file}")
|
||||
run_command(["pdfcrop", str(pdf_file), str(pdf_file)], check=False)
|
||||
|
||||
|
||||
@pdf.command("to-tiff")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option("--density", "-d", default="300", help="DPI density")
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def to_tiff(files, density, dry_run):
|
||||
"""Convert PDF to TIFF."""
|
||||
for pattern in files:
|
||||
for pdf_file in Path(".").glob(pattern):
|
||||
if pdf_file.suffix.lower() != ".pdf":
|
||||
continue
|
||||
output = pdf_file.with_suffix(".tiff")
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would convert: {pdf_file} -> {output}")
|
||||
continue
|
||||
|
||||
click.echo(f"Converting: {pdf_file} -> {output}")
|
||||
run_command(
|
||||
["convert", "-density", density, str(pdf_file), "-compress", "zip", str(output)],
|
||||
check=False,
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
"""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")
|
||||
@@ -0,0 +1,100 @@
|
||||
"""SSH connection utilities."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from mycli.config import config
|
||||
from mycli.utils import handle_errors
|
||||
|
||||
|
||||
def _get_hosts() -> dict:
|
||||
"""Get SSH hosts from config."""
|
||||
return config.get("ssh_hosts", {})
|
||||
|
||||
|
||||
def _build_ssh_base_cmd(host_config: dict) -> list[str]:
|
||||
"""Build base SSH command with host and port."""
|
||||
cmd = ["ssh", "-p", str(host_config["port"])]
|
||||
|
||||
if "key" in host_config:
|
||||
key_path = Path(host_config["key"]).expanduser()
|
||||
cmd.extend(["-i", str(key_path)])
|
||||
|
||||
cmd.append(host_config["host"])
|
||||
return cmd
|
||||
|
||||
|
||||
@click.group(name="ssh")
|
||||
def ssh_cmd():
|
||||
"""SSH connection utilities."""
|
||||
pass
|
||||
|
||||
|
||||
@ssh_cmd.command("connect")
|
||||
@click.argument("name")
|
||||
@handle_errors
|
||||
def connect(name):
|
||||
"""Connect to a configured host."""
|
||||
hosts = _get_hosts()
|
||||
if name not in hosts:
|
||||
click.echo(f"Unknown host: {name}", err=True)
|
||||
click.echo(f"Available: {', '.join(hosts.keys())}", err=True)
|
||||
raise click.Exit(1)
|
||||
|
||||
cmd = _build_ssh_base_cmd(hosts[name])
|
||||
click.secho(f"Connecting to {name}...", fg="cyan")
|
||||
subprocess.run(cmd)
|
||||
|
||||
|
||||
@ssh_cmd.command("tunnel")
|
||||
@click.argument("name")
|
||||
@click.option("--local-port", "-l", default=11111, help="Local port")
|
||||
@click.option("--remote-port", "-r", default=11111, help="Remote port")
|
||||
@handle_errors
|
||||
def tunnel(name, local_port, remote_port):
|
||||
"""Create SSH tunnel with port forwarding."""
|
||||
hosts = _get_hosts()
|
||||
if name not in hosts:
|
||||
click.echo(f"Unknown host: {name}", err=True)
|
||||
raise click.Exit(1)
|
||||
|
||||
host_config = hosts[name]
|
||||
cmd = ["ssh", "-p", str(host_config["port"]), "-L", f"{local_port}:localhost:{remote_port}"]
|
||||
|
||||
if "key" in host_config:
|
||||
key_path = Path(host_config["key"]).expanduser()
|
||||
cmd.extend(["-i", str(key_path)])
|
||||
|
||||
cmd.append(host_config["host"])
|
||||
|
||||
click.secho(f"Creating tunnel {local_port} -> {remote_port} on {name}...", fg="cyan")
|
||||
click.secho(f"Command: {' '.join(cmd)}", fg="dim")
|
||||
subprocess.run(cmd)
|
||||
|
||||
|
||||
@ssh_cmd.command("list")
|
||||
def list_hosts():
|
||||
"""List configured SSH hosts."""
|
||||
hosts = _get_hosts()
|
||||
click.secho("Configured hosts:", fg="cyan")
|
||||
for name, host_config in sorted(hosts.items()):
|
||||
click.echo(f" {name:12} {host_config['host']}:{host_config['port']}")
|
||||
|
||||
|
||||
@ssh_cmd.command("cmd")
|
||||
@click.argument("name")
|
||||
@click.argument("command")
|
||||
@handle_errors
|
||||
def run_cmd(name, command):
|
||||
"""Run a command on remote host."""
|
||||
hosts = _get_hosts()
|
||||
if name not in hosts:
|
||||
click.echo(f"Unknown host: {name}", err=True)
|
||||
raise click.Exit(1)
|
||||
|
||||
cmd = _build_ssh_base_cmd(hosts[name])
|
||||
cmd.append(command)
|
||||
|
||||
subprocess.run(cmd)
|
||||
@@ -0,0 +1,32 @@
|
||||
"""Utility commands."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from mycli.utils import handle_errors
|
||||
|
||||
|
||||
@click.group()
|
||||
def utils():
|
||||
"""Miscellaneous utilities."""
|
||||
pass
|
||||
|
||||
|
||||
@utils.command("albany-cleanup")
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def albany_cleanup(dry_run):
|
||||
"""Clean up Albany temporary files (phalanx_*)."""
|
||||
count = 0
|
||||
for file in Path(".").glob("phalanx_*"):
|
||||
if dry_run:
|
||||
click.echo(f"Would remove: {file}")
|
||||
else:
|
||||
file.unlink()
|
||||
count += 1
|
||||
|
||||
if dry_run:
|
||||
click.echo("Dry run completed")
|
||||
else:
|
||||
click.echo(f"Removed {count} Albany temporary files")
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Video utilities."""
|
||||
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
from mycli.utils import handle_errors, run_command
|
||||
|
||||
|
||||
@click.group()
|
||||
def video():
|
||||
"""Video conversion commands."""
|
||||
pass
|
||||
|
||||
|
||||
@video.command("avi-to-mp4")
|
||||
@click.argument("files", nargs=-1, required=True)
|
||||
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
|
||||
@handle_errors
|
||||
def avi_to_mp4(files, dry_run):
|
||||
"""Convert AVI files to MP4 using ffmpeg."""
|
||||
for pattern in files:
|
||||
for avi_file in Path(".").glob(pattern):
|
||||
if avi_file.suffix.lower() != ".avi":
|
||||
continue
|
||||
output = avi_file.with_suffix(".mp4")
|
||||
|
||||
if dry_run:
|
||||
click.echo(f"Would convert: {avi_file} -> {output}")
|
||||
continue
|
||||
|
||||
click.echo(f"Converting: {avi_file} -> {output}")
|
||||
run_command(
|
||||
["ffmpeg", "-i", str(avi_file), "-c:v", "libx264", "-c:a", "aac", str(output)],
|
||||
check=False,
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"paths": {
|
||||
"study": "~/Documents/myStudy/self_learning",
|
||||
"research": "~/Documents/myResearch",
|
||||
"apaam": "~/Documents/myResearch/myProjects/apaam",
|
||||
"tianhe": "~/Documents/myWork/sysu/0_中山大学/hpc_tianhe",
|
||||
"academia": "~/Library/Mobile Documents/iCloud~md~obsidian/Documents/myacademia",
|
||||
"webpage": "~/Documents/myWork/sysu/4_成果归档/mywebpage",
|
||||
"slides": "~/Documents/myWork/sysu/4_成果归档/myslides",
|
||||
"myclaude": "~/Documents/myResearch/myProjects/apaam/repo/myclaude",
|
||||
"myagent": "~/Documents/myResearch/myProjects/apaam/repo/myagent"
|
||||
},
|
||||
"ssh_hosts": {
|
||||
"tianhe": {
|
||||
"host": "sysu_lchuangxy_1@172.16.31.31",
|
||||
"port": 6666,
|
||||
"key": "~/Documents/myWork/sysu/0_中山大学/hpc_tianhe/sysu_lchuangxy_1.id"
|
||||
},
|
||||
"starlight": {
|
||||
"host": "sysu_lchuang_1@proxy.nscc-gz.cn",
|
||||
"port": 23
|
||||
}
|
||||
},
|
||||
"git_proxies": {
|
||||
"fastgithub": "http://127.0.0.1:38457",
|
||||
"pandafan": "http://127.0.0.1:10080"
|
||||
},
|
||||
"openfoam": {
|
||||
"dmg_path": "~/Documents/myResearch/myProjects/apaam/repo/OpenFOAM.dmg",
|
||||
"mount_point": "/Volumes/OpenFOAM",
|
||||
"build_path": "/Volumes/OpenFOAM/openfoam/build"
|
||||
},
|
||||
"cv": "~/Documents/Personal/resume_cv/cv_lai_maintaining_latex/cv_lai_maintaining"
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
"""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()
|
||||
@@ -0,0 +1,42 @@
|
||||
"""Utility functions and decorators for mycli."""
|
||||
|
||||
import functools
|
||||
import subprocess
|
||||
|
||||
import click
|
||||
|
||||
|
||||
def handle_errors(func):
|
||||
"""Decorator to handle common errors gracefully."""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except FileNotFoundError as e:
|
||||
click.secho(f"Command not found: {e.filename}", fg="red", err=True)
|
||||
raise click.Exit(1)
|
||||
except subprocess.CalledProcessError as e:
|
||||
click.secho(f"Command failed with exit code {e.returncode}", fg="red", err=True)
|
||||
if e.stderr:
|
||||
click.echo(e.stderr, err=True)
|
||||
raise click.Exit(1)
|
||||
except KeyboardInterrupt:
|
||||
click.echo("\nAborted.")
|
||||
raise click.Exit(130)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def run_command(cmd: list[str], check: bool = True, **kwargs) -> subprocess.CompletedProcess:
|
||||
"""Run a subprocess command with consistent error handling.
|
||||
|
||||
Args:
|
||||
cmd: Command and arguments as list
|
||||
check: Whether to check return code
|
||||
**kwargs: Additional args for subprocess.run
|
||||
|
||||
Returns:
|
||||
CompletedProcess instance
|
||||
"""
|
||||
return subprocess.run(cmd, check=check, **kwargs)
|
||||
@@ -0,0 +1,15 @@
|
||||
[project]
|
||||
name = "mycli"
|
||||
version = "0.1.0"
|
||||
description = "Personal CLI toolkit"
|
||||
requires-python = ">=3.9"
|
||||
dependencies = [
|
||||
"click>=8.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
mycli = "mycli.cli:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
@@ -0,0 +1,58 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.9"
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.10'",
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.8"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version < '3.10' and sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593, upload-time = "2024-12-21T18:38:44.339Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188, upload-time = "2024-12-21T18:38:41.666Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version >= '3.10'",
|
||||
]
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "python_full_version >= '3.10' and sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mycli"
|
||||
version = "0.1.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "click", version = "8.1.8", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" },
|
||||
{ name = "click", version = "8.3.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [{ name = "click", specifier = ">=8.0" }]
|
||||
Reference in New Issue
Block a user