refactor: rename bin/ package to myclaude/
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"""CLI subcommands."""
|
||||
|
||||
import click
|
||||
|
||||
from .update import update_cmd
|
||||
|
||||
__all__ = ["update_cmd", "upgrade_cmd"]
|
||||
|
||||
|
||||
@click.command("upgrade")
|
||||
def upgrade_cmd() -> None:
|
||||
"""Alias for update: reinstall myclaude from this repository."""
|
||||
return click.get_current_context().invoke(update_cmd)
|
||||
@@ -0,0 +1,188 @@
|
||||
"""Sync workspace symlinks from zshrc environment variables."""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from myclaude.project_root import get_myclaude_project_root, get_workspace_root
|
||||
|
||||
console = Console()
|
||||
stderr_console = Console(stderr=True)
|
||||
|
||||
# Link name -> list of environment variable names to try, in order.
|
||||
#
|
||||
# Customize this dict for your own projects. Each entry creates a symlink
|
||||
# from workspace/<name> to the target path resolved from one of the env vars.
|
||||
# Users define the corresponding env vars in their shell config (e.g. ~/.zshrc).
|
||||
_LINK_MAP: dict[str, list[str]] = {
|
||||
"myacademia": ["path_myacademia"],
|
||||
"myslides": ["path_myslides", "MYSLIDES_ROOT"],
|
||||
"metabot": ["METABOT_HOME"],
|
||||
"mytoolkit": ["path_mytoolkit", "MYTOOLKIT_ROOT"],
|
||||
"mywebpage": ["path_mywebpage", "MYWEBPAGE_ROOT"],
|
||||
}
|
||||
|
||||
|
||||
def _read_zsh_env(var: str, zshrc: Path) -> str | None:
|
||||
"""Source zshrc and print the value of a variable."""
|
||||
cmd = [
|
||||
"zsh",
|
||||
"-c",
|
||||
f"source '{zshrc}' >/dev/null 2>&1; echo -n ${var}",
|
||||
]
|
||||
try:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
value = result.stdout.strip()
|
||||
return value if value else None
|
||||
|
||||
|
||||
def _resolve_target(
|
||||
env_vars: list[str],
|
||||
zshrc: Path,
|
||||
) -> Path | None:
|
||||
"""Find the first valid target path from environment variables."""
|
||||
for var in env_vars:
|
||||
raw = _read_zsh_env(var, zshrc)
|
||||
if raw is None:
|
||||
continue
|
||||
expanded = os.path.expandvars(os.path.expanduser(raw))
|
||||
path = Path(expanded)
|
||||
if path.exists():
|
||||
return path.resolve()
|
||||
return None
|
||||
|
||||
|
||||
def _current_link_target(workspace: Path, name: str) -> Path | None:
|
||||
"""Return the target of an existing symlink, or None."""
|
||||
link = workspace / name
|
||||
if link.is_symlink():
|
||||
try:
|
||||
return Path(os.readlink(link))
|
||||
except OSError:
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def _sync_link(
|
||||
workspace: Path,
|
||||
name: str,
|
||||
target: Path,
|
||||
dry_run: bool,
|
||||
force: bool,
|
||||
) -> bool:
|
||||
"""Create or update a symlink. Returns True if changed."""
|
||||
link = workspace / name
|
||||
current = _current_link_target(workspace, name)
|
||||
|
||||
if current is not None and current.resolve() == target.resolve():
|
||||
return False
|
||||
|
||||
if link.exists() and not link.is_symlink():
|
||||
if not force:
|
||||
stderr_console.print(
|
||||
f"[yellow]{name} exists but is not a symlink. "
|
||||
"Use --force to replace.[/yellow]"
|
||||
)
|
||||
return False
|
||||
if dry_run:
|
||||
console.print(
|
||||
f"[dry-run] Would remove {name} and symlink -> {target}"
|
||||
)
|
||||
return True
|
||||
if link.is_dir():
|
||||
link.rmdir()
|
||||
else:
|
||||
link.unlink()
|
||||
elif current is not None:
|
||||
if dry_run:
|
||||
console.print(
|
||||
f"[dry-run] Would update {name}: {current} -> {target}"
|
||||
)
|
||||
return True
|
||||
link.unlink()
|
||||
else:
|
||||
if dry_run:
|
||||
console.print(f"[dry-run] Would create {name} -> {target}")
|
||||
return True
|
||||
|
||||
link.symlink_to(target, target_is_directory=target.is_dir())
|
||||
return True
|
||||
|
||||
|
||||
def sync_workspace_links(
|
||||
zshrc: Path | None = None,
|
||||
dry_run: bool = False,
|
||||
force: bool = False,
|
||||
verbose: bool = True,
|
||||
) -> bool:
|
||||
"""Sync all workspace symlinks. Returns True on success."""
|
||||
workspace = get_workspace_root()
|
||||
if zshrc is None:
|
||||
zshrc = Path.home() / ".zshrc"
|
||||
|
||||
if verbose:
|
||||
console.print(f"[bold]Workspace:[/bold] {workspace}")
|
||||
console.print(f"[bold]Zshrc:[/bold] {zshrc}")
|
||||
console.print()
|
||||
|
||||
changed = 0
|
||||
skipped = 0
|
||||
broken = 0
|
||||
|
||||
# Handle myclaude separately: use project root detection, not zshrc.
|
||||
myclaude_target = get_myclaude_project_root()
|
||||
if _sync_link(workspace, "myclaude", myclaude_target, dry_run, force):
|
||||
action = "would update" if dry_run else "updated"
|
||||
if verbose:
|
||||
console.print(
|
||||
f"[green]myclaude: {action} -> {myclaude_target}[/green]"
|
||||
)
|
||||
changed += 1
|
||||
else:
|
||||
if verbose:
|
||||
console.print("[dim]myclaude: already correct[/dim]")
|
||||
|
||||
for name, env_vars in _LINK_MAP.items():
|
||||
target = _resolve_target(env_vars, zshrc)
|
||||
if target is None:
|
||||
link = workspace / name
|
||||
if link.is_symlink() and not link.exists():
|
||||
if verbose:
|
||||
stderr_console.print(
|
||||
f"[red]{name}: broken symlink (no env var found)[/red]"
|
||||
)
|
||||
broken += 1
|
||||
else:
|
||||
if verbose:
|
||||
console.print(f"[dim]{name}: skipped (no env var)[/dim]")
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
if _sync_link(workspace, name, target, dry_run, force):
|
||||
action = "would update" if dry_run else "updated"
|
||||
if verbose:
|
||||
console.print(f"[green]{name}: {action} -> {target}[/green]")
|
||||
changed += 1
|
||||
else:
|
||||
if verbose:
|
||||
console.print(f"[dim]{name}: already correct[/dim]")
|
||||
|
||||
if verbose:
|
||||
console.print()
|
||||
label = "Preview" if dry_run else "Done"
|
||||
console.print(
|
||||
f"[bold]{label}:[/bold] {changed} changed, "
|
||||
f"{skipped} skipped, {broken} broken."
|
||||
)
|
||||
|
||||
return broken == 0
|
||||
@@ -0,0 +1,61 @@
|
||||
"""Reinstall myclaude from the local repo."""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
from rich.console import Console
|
||||
|
||||
from myclaude.commands.init import sync_workspace_links
|
||||
from myclaude.project_root import get_myclaude_project_root
|
||||
|
||||
console = Console()
|
||||
stderr_console = Console(stderr=True)
|
||||
|
||||
|
||||
def _run_make_install(root: Path) -> int:
|
||||
return subprocess.run(
|
||||
["make", "install"],
|
||||
cwd=str(root),
|
||||
check=False,
|
||||
).returncode
|
||||
|
||||
|
||||
def _run_pip_editable(root: Path) -> int:
|
||||
cmd = [sys.executable, "-m", "pip", "install", "-e", str(root)]
|
||||
return subprocess.run(cmd, check=False).returncode
|
||||
|
||||
|
||||
@click.command("update")
|
||||
def update_cmd() -> None:
|
||||
"""Reinstall myclaude and sync workspace symlinks."""
|
||||
root = get_myclaude_project_root()
|
||||
fallback = Path.home() / ".myclaude"
|
||||
if root == fallback:
|
||||
stderr_console.print(
|
||||
"[red]Not inside myclaude repo.[/red] Set [cyan]MYCLAUDE_PROJECT_ROOT[/cyan] "
|
||||
"or run from the clone.",
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
console.print("[bold cyan]Updating myclaude…[/bold cyan]")
|
||||
makefile = root / "Makefile"
|
||||
if makefile.is_file():
|
||||
rc = _run_make_install(root)
|
||||
if rc != 0:
|
||||
console.print(
|
||||
"[yellow]make install failed, trying pip -e…[/yellow]"
|
||||
)
|
||||
rc = _run_pip_editable(root)
|
||||
else:
|
||||
rc = _run_pip_editable(root)
|
||||
|
||||
if rc != 0:
|
||||
stderr_console.print("[red]Update failed.[/red]")
|
||||
raise SystemExit(rc)
|
||||
console.print("[green]myclaude updated.[/green]")
|
||||
|
||||
console.print()
|
||||
console.print("[bold cyan]Syncing workspace links…[/bold cyan]")
|
||||
sync_workspace_links()
|
||||
Reference in New Issue
Block a user