Skip to content
Open
55 changes: 23 additions & 32 deletions graphify/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,12 +200,8 @@ def _refresh_all_version_stamps() -> None:

def _platform_skill_destination(platform_name: str, *, project: bool = False, project_dir: Path | None = None) -> Path:
"""Return the skill destination for a platform and scope."""
if platform_name == "gemini":
if project:
return (project_dir or Path(".")) / ".gemini" / "skills" / "graphify" / "SKILL.md"
if platform.system() == "Windows":
return Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md"
return Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
if platform_name in ("gemini", "antigravity", "antigravity-windows"):
return Path.home() / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md"

if platform_name == "opencode":
if project:
Expand Down Expand Up @@ -239,12 +235,6 @@ def _platform_skill_destination(platform_name: str, *, project: bool = False, pr
return (project_dir or Path(".")) / ".agents" / "skills" / "graphify" / "SKILL.md"
return Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md"

if platform_name in ("antigravity", "antigravity-windows"):
if project:
return (project_dir or Path(".")) / ".agents" / "skills" / "graphify" / "SKILL.md"
# Global Antigravity skill dir (all workspaces): ~/.gemini/config/skills/
return Path.home() / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md"

cfg = _PLATFORM_CONFIG[platform_name]
if project:
return (project_dir or Path(".")) / cfg["skill_dst"]
Expand Down Expand Up @@ -1218,26 +1208,27 @@ def _antigravity_uninstall(project_dir: Path, *, project: bool = False) -> None:
wf_path.unlink()
print(f"graphify workflow removed from {wf_path.resolve()}")

# Remove skill file
skill_dst = _platform_skill_destination("antigravity", project=project, project_dir=project_dir)
if skill_dst.exists():
skill_dst.unlink()
print(f"graphify skill removed from {skill_dst}")
version_file = skill_dst.parent / ".graphify_version"
if version_file.exists():
version_file.unlink()
refs_dir = skill_dst.parent / "references"
if refs_dir.exists():
shutil.rmtree(refs_dir)
for d in (
skill_dst.parent,
skill_dst.parent.parent,
skill_dst.parent.parent.parent,
):
try:
d.rmdir()
except OSError:
break
# Remove skill file only for global uninstall
if not project:
skill_dst = _platform_skill_destination("antigravity", project=project, project_dir=project_dir)
if skill_dst.exists():
skill_dst.unlink()
print(f"graphify skill removed from {skill_dst}")
version_file = skill_dst.parent / ".graphify_version"
if version_file.exists():
version_file.unlink()
refs_dir = skill_dst.parent / "references"
if refs_dir.exists():
shutil.rmtree(refs_dir)
for d in (
skill_dst.parent,
skill_dst.parent.parent,
skill_dst.parent.parent.parent,
):
try:
d.rmdir()
except OSError:
break


_CURSOR_RULE_PATH = Path(".cursor") / "rules" / "graphify.mdc"
Expand Down
36 changes: 26 additions & 10 deletions tests/test_antigravity_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,37 @@
import graphify.__main__ as m


from unittest.mock import patch

def test_antigravity_project_install_writes_rules_and_workflows(tmp_path):
m._project_install("antigravity", tmp_path)
skill = tmp_path / ".agents" / "skills" / "graphify" / "SKILL.md"
rules = tmp_path / ".agents" / "rules" / "graphify.md"
workflow = tmp_path / ".agents" / "workflows" / "graphify.md"
assert skill.exists(), "skill should be installed under .agents/skills/"
home = tmp_path / "home"
proj = tmp_path / "proj"
home.mkdir()
proj.mkdir()
with patch("graphify.__main__.Path.home", return_value=home):
m._project_install("antigravity", proj)

skill = home / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md"
rules = proj / ".agents" / "rules" / "graphify.md"
workflow = proj / ".agents" / "workflows" / "graphify.md"

assert skill.exists(), "skill should be installed globally under ~/.gemini/config/skills/"
assert rules.exists(), "antigravity rules (always-on) must be written"
assert workflow.exists(), "antigravity workflow must be written"
# native tool-discovery frontmatter is injected into the skill
assert skill.read_text(encoding="utf-8").startswith("---\n")


def test_antigravity_project_uninstall_clears_rules_and_workflows(tmp_path):
m._project_install("antigravity", tmp_path)
m._project_uninstall("antigravity", tmp_path)
assert not (tmp_path / ".agents" / "rules" / "graphify.md").exists()
assert not (tmp_path / ".agents" / "workflows" / "graphify.md").exists()
assert not (tmp_path / ".agents" / "skills" / "graphify" / "SKILL.md").exists()
home = tmp_path / "home"
proj = tmp_path / "proj"
home.mkdir()
proj.mkdir()
with patch("graphify.__main__.Path.home", return_value=home):
m._project_install("antigravity", proj)
m._project_uninstall("antigravity", proj)

assert not (proj / ".agents" / "rules" / "graphify.md").exists()
assert not (proj / ".agents" / "workflows" / "graphify.md").exists()
# The global skill should remain intact after a project uninstall.
assert (home / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md").exists()
14 changes: 6 additions & 8 deletions tests/test_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ def test_codex_subcommand_project_install_and_uninstall_are_project_scoped(tmp_p
assert "graphify" not in hooks_path.read_text()


def test_antigravity_install_project_writes_project_skill(tmp_path, monkeypatch):
def test_antigravity_install_project_writes_global_skill(tmp_path, monkeypatch):
from graphify.__main__ import main
home = tmp_path / "home"
project = tmp_path / "project"
Expand All @@ -162,8 +162,8 @@ def test_antigravity_install_project_writes_project_skill(tmp_path, monkeypatch)
monkeypatch.setattr(sys, "argv", ["graphify", "antigravity", "install", "--project"])
with patch("graphify.__main__.Path.home", return_value=home):
main()
assert (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists()
assert not (home / ".agents" / "skills" / "graphify" / "SKILL.md").exists()
assert (home / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md").exists()
assert not (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists()


def test_install_help_does_not_install_default(tmp_path, monkeypatch, capsys):
Expand Down Expand Up @@ -225,7 +225,7 @@ def test_codex_skill_uses_graphify_with_existing_graph():
fast-path block, which jumps straight to the query flow when a graph exists.
"""
import graphify
skill = (Path(graphify.__file__).parent / "skill-codex.md").read_text()
skill = (Path(graphify.__file__).parent / "skill-codex.md").read_text(encoding="utf-8")
assert "Fast path — existing graph" in skill
assert "skip Steps 1–5 entirely and jump straight to `## For /graphify query`" in skill
assert "graphify query" in skill
Expand Down Expand Up @@ -474,12 +474,11 @@ def test_uninstall_project_without_platform_removes_project_installs(tmp_path, m
assert not (project / ".claude" / "CLAUDE.md").exists()


def test_antigravity_uninstall_project_removes_project_skill_only(tmp_path, monkeypatch):
def test_antigravity_uninstall_project_preserves_global_skill(tmp_path, monkeypatch):
from graphify.__main__ import main
home = tmp_path / "home"
project = tmp_path / "project"
project.mkdir()
# Global skill lives at ~/.gemini/config/skills/ (per #1079 fix)
global_skill = home / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md"
global_skill.parent.mkdir(parents=True)
global_skill.write_text("global skill")
Expand All @@ -490,7 +489,6 @@ def test_antigravity_uninstall_project_removes_project_skill_only(tmp_path, monk
monkeypatch.setattr(sys, "argv", ["graphify", "antigravity", "uninstall", "--project"])
main()
assert global_skill.exists(), "project uninstall must not touch global skill"
assert not (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists()


def test_antigravity_global_install_writes_gemini_config_skills(tmp_path, monkeypatch):
Expand Down Expand Up @@ -1056,4 +1054,4 @@ def test_hermes_skill_destination_posix_uses_home():
from graphify.__main__ import _platform_skill_destination
with patch("graphify.__main__.platform.system", return_value="Linux"):
dst = _platform_skill_destination("hermes", project=False)
assert str(dst).endswith(".hermes/skills/graphify/SKILL.md"), dst
assert dst.as_posix().endswith(".hermes/skills/graphify/SKILL.md"), dst
2 changes: 1 addition & 1 deletion tests/test_install_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,7 @@ def test_gemini_install_references_all_resolve(tmp_path):
"""
import re
_install(tmp_path, "gemini")
skill = tmp_path / ".gemini" / "skills" / "graphify" / "SKILL.md"
skill = tmp_path / ".gemini" / "config" / "skills" / "graphify" / "SKILL.md"
assert skill.exists()
refdir = skill.parent / "references"
assert refdir.is_dir()
Expand Down
67 changes: 34 additions & 33 deletions tests/test_install_roundtrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,39 +59,40 @@ def test_skill_roundtrip_at_real_destination(platform, project, tmp_path, monkey
monkeypatch.chdir(project_dir)

with patch("graphify.__main__.Path.home", return_value=home):
dst = mainmod._platform_skill_destination(
platform, project=project, project_dir=project_dir
)
# Sanity: a user-scope install must not write under the project dir, and
# vice versa, so the two scopes never collide in this test.
if project:
assert str(dst).startswith(str(project_dir))
else:
assert str(dst).startswith(str(home))

returned = mainmod._copy_skill_file(
platform, project=project, project_dir=project_dir
)
assert returned == dst
assert dst.exists(), f"{platform} ({'project' if project else 'user'}) skill not installed"
assert (dst.parent / ".graphify_version").read_text() == mainmod.__version__

refs = dst.parent / "references"
if _has_real_bundle(platform):
assert refs.is_dir(), f"{platform} ships a bundle but no references/ installed"
assert (refs / "extraction-spec.md").exists()
else:
assert not refs.exists(), f"{platform} is monolith but references/ appeared"
# No staging dir is ever left behind.
assert not (dst.parent / "references.tmp").exists()

removed = mainmod._remove_skill_file(
platform, project=project, project_dir=project_dir
)
assert removed
assert not dst.exists()
assert not (dst.parent / ".graphify_version").exists()
assert not refs.exists()
with patch.dict("os.environ", {"LOCALAPPDATA": str(home / "AppData" / "Local")}):
dst = mainmod._platform_skill_destination(
platform, project=project, project_dir=project_dir
)
# Sanity: a user-scope install must not write under the project dir, and
# vice versa, so the two scopes never collide in this test.
if project and platform not in ("antigravity", "antigravity-windows", "gemini"):
assert str(dst).startswith(str(project_dir))
else:
assert str(dst).startswith(str(home))

returned = mainmod._copy_skill_file(
platform, project=project, project_dir=project_dir
)
assert returned == dst
assert dst.exists(), f"{platform} ({'project' if project else 'user'}) skill not installed"
assert (dst.parent / ".graphify_version").read_text() == mainmod.__version__

refs = dst.parent / "references"
if _has_real_bundle(platform):
assert refs.is_dir(), f"{platform} ships a bundle but no references/ installed"
assert (refs / "extraction-spec.md").exists()
else:
assert not refs.exists(), f"{platform} is monolith but references/ appeared"
# No staging dir is ever left behind.
assert not (dst.parent / "references.tmp").exists()

removed = mainmod._remove_skill_file(
platform, project=project, project_dir=project_dir
)
assert removed
assert not dst.exists()
assert not (dst.parent / ".graphify_version").exists()
assert not refs.exists()


def test_amp_user_install_at_corrected_agents_path(tmp_path, monkeypatch):
Expand Down
Loading