fix Windows Unicode crash on non-UTF-8 consoles (issue #992)

reconfigure stdout/stderr to UTF-8 at startup; replace → and — in all
print statements with ASCII equivalents as belt-and-suspenders fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Safi
2026-05-31 00:06:35 +01:00
co-authored by Claude Sonnet 4.6
parent 55d7dadb63
commit 31a608f73e
3 changed files with 23 additions and 17 deletions
+16 -10
View File
@@ -1118,7 +1118,7 @@ def _agents_install(project_dir: Path, platform: str) -> None:
if platform not in ("codex", "opencode"):
print()
print("Note: unlike Claude Code, there is no PreToolUse hook equivalent for")
print(f"{platform.capitalize()} — the AGENTS.md rules are the always-on mechanism.")
print(f"{platform.capitalize()} - the AGENTS.md rules are the always-on mechanism.")
def _project_install(platform_name: str, project_dir: Path | None = None) -> None:
@@ -1399,7 +1399,7 @@ def _clone_repo(url: str, branch: str | None = None, out_dir: Path | None = None
sys.exit(1)
if dest.exists():
print(f"Repo already cloned at {dest} — pulling latest...", flush=True)
print(f"Repo already cloned at {dest} - pulling latest...", flush=True)
cmd = ["git", "-C", str(dest), "pull"]
if branch:
cmd += ["origin", "--", branch]
@@ -1423,6 +1423,12 @@ def _clone_repo(url: str, branch: str | None = None, out_dir: Path | None = None
def main() -> None:
for _stream in (sys.stdout, sys.stderr):
if _stream is not None and hasattr(_stream, "reconfigure"):
try:
_stream.reconfigure(encoding="utf-8", errors="replace")
except Exception:
pass
# Check all known skill install locations for a stale version stamp.
# Skip during install/uninstall (hook writes trigger a fresh check anyway).
# Skip during hook-check — it runs on every editor tool use and must be silent.
@@ -2364,7 +2370,7 @@ def main() -> None:
watch_path = Path(".")
graph_json = graph_override if graph_override is not None else watch_path / "graphify-out" / "graph.json"
if not graph_json.exists():
print(f"error: no graph found at {graph_json} — run /graphify first", file=sys.stderr)
print(f"error: no graph found at {graph_json} - run /graphify first", file=sys.stderr)
sys.exit(1)
from networkx.readwrite import json_graph as _jg
from graphify.build import build_from_json
@@ -2427,16 +2433,16 @@ def main() -> None:
if no_viz:
if html_target.exists():
html_target.unlink()
print(f"Done — {len(communities)} communities. GRAPH_REPORT.md and graph.json updated (--no-viz; graph.html removed).")
print(f"Done - {len(communities)} communities. GRAPH_REPORT.md and graph.json updated (--no-viz; graph.html removed).")
else:
try:
to_html(G, communities, str(html_target), community_labels=labels or None)
print(f"Done — {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.")
print(f"Done - {len(communities)} communities. GRAPH_REPORT.md, graph.json and graph.html updated.")
except ValueError as viz_err:
if html_target.exists():
html_target.unlink()
print(f"Skipped graph.html: {viz_err}")
print(f"Done — {len(communities)} communities. GRAPH_REPORT.md and graph.json updated.")
print(f"Done - {len(communities)} communities. GRAPH_REPORT.md and graph.json updated.")
elif cmd == "update":
force = os.environ.get("GRAPHIFY_FORCE", "").lower() in ("1", "true", "yes")
@@ -2487,7 +2493,7 @@ def main() -> None:
):
print("Tip: set GEMINI_API_KEY or GOOGLE_API_KEY to use Gemini for semantic extraction.")
else:
print("Nothing to update or rebuild failed — check output above.", file=sys.stderr)
print("Nothing to update or rebuild failed - check output above.", file=sys.stderr)
sys.exit(1)
elif cmd == "hook-check":
@@ -2981,7 +2987,7 @@ def main() -> None:
try:
result = _global_add(source, tag)
if result["skipped"]:
print(f"'{tag}' unchanged since last add — global graph not modified.")
print(f"'{tag}' unchanged since last add - global graph not modified.")
else:
print(f"Added '{tag}' to global graph: +{result['nodes_added']} nodes, "
f"-{result['nodes_removed']} pruned. Global: {_global_path()}")
@@ -3458,7 +3464,7 @@ def main() -> None:
try:
result = _global_add(graphify_out / "graph.json", _tag)
if result["skipped"]:
print(f"[graphify global] '{_tag}' unchanged since last add — skipped.")
print(f"[graphify global] '{_tag}' unchanged since last add - skipped.")
else:
print(f"[graphify global] '{_tag}' merged into global graph "
f"(+{result['nodes_added']} nodes, -{result['nodes_removed']} pruned).")
@@ -3520,7 +3526,7 @@ def main() -> None:
try:
result = _global_add(graphify_out / "graph.json", _tag)
if result["skipped"]:
print(f"[graphify global] '{_tag}' unchanged since last add — skipped.")
print(f"[graphify global] '{_tag}' unchanged since last add - skipped.")
else:
print(f"[graphify global] '{_tag}' merged into global graph "
f"(+{result['nodes_added']} nodes, -{result['nodes_removed']} pruned).")
+2 -2
View File
@@ -86,11 +86,11 @@ def backup_if_protected(out_dir: Path) -> "Path | None":
except Exception:
pass
if copied:
print(f"[graphify] backed up {reason} graph ({copied} files) → {backup_dir.name}/")
print(f"[graphify] backed up {reason} graph ({copied} files) -> {backup_dir.name}/")
return backup_dir
except Exception as exc:
import sys
print(f"[graphify] warning: backup failed ({exc}) — continuing with overwrite", file=sys.stderr)
print(f"[graphify] warning: backup failed ({exc}) - continuing with overwrite", file=sys.stderr)
return None
def _obsidian_tag(name: str) -> str:
+5 -5
View File
@@ -471,7 +471,7 @@ def render_worktrees(prs: list[PRInfo], worktrees: dict[str, str]) -> None:
if pr:
status = _status_color(pr.status)
print(f" {cyan(path)}")
print(f" {dim('branch:')} {branch} → PR {bold(f'#{pr.number}')} [{status}] {_truncate(pr.title, 50)}")
print(f" {dim('branch:')} {branch} -> PR {bold(f'#{pr.number}')} [{status}] {_truncate(pr.title, 50)}")
else:
print(f" {cyan(path)}")
print(f" {dim('branch:')} {branch} {dim('(no open PR)')}")
@@ -485,7 +485,7 @@ def render_conflicts(
) -> None:
actionable = [p for p in prs if p.base_branch == base and p.communities_touched]
if not actionable:
print(dim("\n No graph impact data — run with a valid graph.json to detect conflicts.\n"))
print(dim("\n No graph impact data - run with a valid graph.json to detect conflicts.\n"))
return
# Build community → [PRs] map
@@ -496,7 +496,7 @@ def render_conflicts(
conflicts = {c: ps for c, ps in comm_to_prs.items() if len(ps) > 1}
if not conflicts:
print(green("\n No community overlap between open PRs — safe to merge in any order.\n"))
print(green("\n No community overlap between open PRs - safe to merge in any order.\n"))
return
print()
@@ -518,7 +518,7 @@ def render_pr_detail(pr: PRInfo, repo: str | None = None) -> None:
print(bold(f" PR #{pr.number} · {_status_color(pr.status)}"))
print(f" {pr.title}")
print()
print(f" {dim('branch:')} {pr.branch} → {pr.base_branch}")
print(f" {dim('branch:')} {pr.branch} -> {pr.base_branch}")
print(f" {dim('author:')} {pr.author}")
print(f" {dim('updated:')} {pr.days_old}d ago")
print(f" {dim('CI:')} {_ci_icon(pr.ci_status)} {pr.ci_status}")
@@ -579,7 +579,7 @@ def triage_with_opus(prs: list[PRInfo], base: str) -> None:
try:
from graphify.llm import BACKENDS, _get_backend_api_key
except ImportError:
print(red(" graphify.llm not available — cannot run triage."), file=sys.stderr)
print(red(" graphify.llm not available - cannot run triage."), file=sys.stderr)
sys.exit(1)
candidates = [p for p in prs if p.base_branch == base and p.status not in ("WRONG-BASE", "STALE")]