diff --git a/graphify/__main__.py b/graphify/__main__.py index 596d8fe..2a03087 100644 --- a/graphify/__main__.py +++ b/graphify/__main__.py @@ -14,6 +14,10 @@ try: except Exception: __version__ = "unknown" +# Output directory — override with GRAPHIFY_OUT env var for worktrees or shared-output setups. +# Accepts a relative name ("graphify-out-feature") or an absolute path ("/shared/graphify-out"). +_GRAPHIFY_OUT = os.environ.get("GRAPHIFY_OUT", "graphify-out") + def _check_skill_version(skill_dst: Path) -> None: """Warn if the installed skill is from an older graphify version.""" @@ -553,7 +557,12 @@ def _antigravity_install(project_dir: Path) -> None: rules_path = project_dir / _ANTIGRAVITY_RULES_PATH rules_path.parent.mkdir(parents=True, exist_ok=True) if rules_path.exists(): - print(f"graphify rule already exists at {rules_path} (no change)") + existing = rules_path.read_text(encoding="utf-8") + if _ANTIGRAVITY_RULES.strip() != existing.strip(): + rules_path.write_text(_ANTIGRAVITY_RULES, encoding="utf-8") + print(f"graphify rule updated at {rules_path.resolve()}") + else: + print(f"graphify rule already up to date at {rules_path.resolve()}") else: rules_path.write_text(_ANTIGRAVITY_RULES, encoding="utf-8") print(f"graphify rule written to {rules_path.resolve()}") @@ -562,7 +571,12 @@ def _antigravity_install(project_dir: Path) -> None: wf_path = project_dir / _ANTIGRAVITY_WORKFLOW_PATH wf_path.parent.mkdir(parents=True, exist_ok=True) if wf_path.exists(): - print(f"graphify workflow already exists at {wf_path} (no change)") + existing = wf_path.read_text(encoding="utf-8") + if _ANTIGRAVITY_WORKFLOW.strip() != existing.strip(): + wf_path.write_text(_ANTIGRAVITY_WORKFLOW, encoding="utf-8") + print(f"graphify workflow updated at {wf_path.resolve()}") + else: + print(f"graphify workflow already up to date at {wf_path.resolve()}") else: wf_path.write_text(_ANTIGRAVITY_WORKFLOW, encoding="utf-8") print(f"graphify workflow written to {wf_path.resolve()}") @@ -1539,7 +1553,7 @@ def main() -> None: watch_path = Path(argv[2]) else: # Try to recover the scan root saved by the last full build - saved = Path("graphify-out/.graphify_root") + saved = Path(_GRAPHIFY_OUT) / ".graphify_root" if saved.exists(): watch_path = Path(saved.read_text(encoding="utf-8").strip()) else: @@ -1578,7 +1592,7 @@ def main() -> None: # showing top-K outbound edges per symbol. from typing import Optional as _Opt from graphify.tree_html import write_tree_html, DEFAULT_MAX_CHILDREN - graph_path = Path("graphify-out/graph.json") + graph_path = Path(_GRAPHIFY_OUT) / "graph.json" output_path: "_Opt[Path]" = None root: "_Opt[str]" = None max_children = DEFAULT_MAX_CHILDREN @@ -1630,7 +1644,7 @@ def main() -> None: # graphify merge-graphs graph1.json graph2.json ... --out merged.json args = sys.argv[2:] graph_paths: list[Path] = [] - out_path = Path("graphify-out/merged-graph.json") + out_path = Path(_GRAPHIFY_OUT) / "merged-graph.json" i = 0 while i < len(args): if args[i] == "--out" and i + 1 < len(args): diff --git a/graphify/cache.py b/graphify/cache.py index 1dedcac..ef7582d 100644 --- a/graphify/cache.py +++ b/graphify/cache.py @@ -7,6 +7,11 @@ import os import tempfile from pathlib import Path +# Output directory name — override with GRAPHIFY_OUT env var for worktrees or +# shared-output setups. Accepts a relative name ("graphify-out-feature") or an +# absolute path ("/shared/graphify-out"). +_GRAPHIFY_OUT = os.environ.get("GRAPHIFY_OUT", "graphify-out") + def _body_content(content: bytes) -> bytes: """Strip YAML frontmatter from Markdown content, returning only the body.""" @@ -62,7 +67,9 @@ def cache_dir(root: Path = Path("."), kind: str = "ast") -> Path: kind is "ast" or "semantic". Separate subdirectories prevent semantic cache entries from overwriting AST cache entries for the same source_file (#582). """ - d = Path(root).resolve() / "graphify-out" / "cache" / kind + _out = Path(_GRAPHIFY_OUT) + base = _out if _out.is_absolute() else Path(root).resolve() / _out + d = base / "cache" / kind d.mkdir(parents=True, exist_ok=True) return d @@ -89,7 +96,7 @@ def load_cached(path: Path, root: Path = Path("."), kind: str = "ast") -> dict | return None # Migration fallback: check legacy flat cache/ dir for AST entries if kind == "ast": - legacy = Path(root).resolve() / "graphify-out" / "cache" / f"{h}.json" + legacy = Path(root).resolve() / _GRAPHIFY_OUT / "cache" / f"{h}.json" if legacy.exists(): try: return json.loads(legacy.read_text(encoding="utf-8")) @@ -140,7 +147,7 @@ def save_cached(path: Path, result: dict, root: Path = Path("."), kind: str = "a def cached_files(root: Path = Path(".")) -> set[str]: """Return set of file hashes that have a valid cache entry (any kind).""" - base = Path(root).resolve() / "graphify-out" / "cache" + base = Path(root).resolve() / _GRAPHIFY_OUT / "cache" hashes: set[str] = set() # Legacy flat entries if base.is_dir(): @@ -155,7 +162,7 @@ def cached_files(root: Path = Path(".")) -> set[str]: def clear_cache(root: Path = Path(".")) -> None: """Delete all cache entries (ast/, semantic/, and legacy flat entries).""" - base = Path(root).resolve() / "graphify-out" / "cache" + base = Path(root).resolve() / _GRAPHIFY_OUT / "cache" # Legacy flat entries if base.is_dir(): for f in base.glob("*.json"): diff --git a/graphify/watch.py b/graphify/watch.py index 1249685..88cc8c6 100644 --- a/graphify/watch.py +++ b/graphify/watch.py @@ -1,10 +1,13 @@ # monitor a folder and auto-trigger --update when files change from __future__ import annotations import json +import os import sys import time from pathlib import Path +_GRAPHIFY_OUT = os.environ.get("GRAPHIFY_OUT", "graphify-out") + from graphify.detect import CODE_EXTENSIONS, DOC_EXTENSIONS, PAPER_EXTENSIONS, IMAGE_EXTENSIONS @@ -68,7 +71,7 @@ def _rebuild_code(watch_path: Path, *, follow_symlinks: bool = False, force: boo # Filter by node ID membership in the new AST output, not by file_type — # INFERRED/AMBIGUOUS nodes extracted from code files also carry file_type="code" # and would be wrongly dropped by a file_type-based filter. - out = watch_path / "graphify-out" + out = watch_path / _GRAPHIFY_OUT existing_graph = out / "graph.json" if existing_graph.exists(): try: @@ -159,7 +162,7 @@ def check_update(watch_path: Path) -> bool: re-extraction via `/graphify --update` — this function only signals that the update is needed. """ - flag = Path(watch_path) / "graphify-out" / "needs_update" + flag = Path(watch_path) / _GRAPHIFY_OUT / "needs_update" if flag.exists(): print(f"[graphify check-update] Pending non-code changes in {watch_path}.") print("[graphify check-update] Run `/graphify --update` to apply semantic re-extraction.") @@ -168,7 +171,7 @@ def check_update(watch_path: Path) -> bool: def _notify_only(watch_path: Path) -> None: """Write a flag file and print a notification (fallback for non-code-only corpora).""" - flag = watch_path / "graphify-out" / "needs_update" + flag = watch_path / _GRAPHIFY_OUT / "needs_update" flag.parent.mkdir(parents=True, exist_ok=True) flag.write_text("1", encoding="utf-8") print(f"\n[graphify watch] New or changed files detected in {watch_path}") @@ -213,7 +216,7 @@ def watch(watch_path: Path, debounce: float = 3.0) -> None: return if any(part.startswith(".") for part in path.parts): return - if "graphify-out" in path.parts: + if _GRAPHIFY_OUT in path.parts: return last_trigger = time.monotonic() pending = True