Initial commit: mycli toolkit with all commands
This commit is contained in:
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user