Files
myagents/bin/commands/init.py
T
Zhengshou Lai 0c227cae19 feat(desktop): add Electron desktop pet for Claude CLI
- Electron + React + TypeScript 桌面宠物外壳
- xterm.js + node-pty 直接对接 Claude CLI 进程
- 宠物 orb(80x80)可拖拽,点击展开终端面板
- CRT 复古终端主题(扫描线、文字发光)
- 全局快捷键 Cmd+Shift+C 显隐
- 窗口位置自动保存
- 中华田园犬吉祥物 icon(AI 生成)
- Makefile 新增 `make app` 打包命令
2026-05-27 11:23:14 +08:00

185 lines
5.3 KiB
Python

"""Sync workspace symlinks from zshrc environment variables."""
import os
import subprocess
from pathlib import Path
from rich.console import Console
from bin.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.
_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