feat(install): add project-scoped skill installs (#931)
* feat(install): add project-scoped skill installs * fix(install): cover project-scoped antigravity install * test(install): cover project-scoped platform subcommands --------- Co-authored-by: hanmo1 <hanmo1@lenovo.com>
This commit is contained in:
@@ -98,6 +98,20 @@ graphify install
|
|||||||
|
|
||||||
That's it. Open your AI assistant and type `/graphify .`
|
That's it. Open your AI assistant and type `/graphify .`
|
||||||
|
|
||||||
|
To install the assistant skill into the current repository instead of your user
|
||||||
|
profile, add `--project`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
graphify install --project
|
||||||
|
graphify install --project --platform codex
|
||||||
|
```
|
||||||
|
|
||||||
|
Project-scoped installs write under the current directory, for example
|
||||||
|
`.claude/skills/graphify/SKILL.md` or `.agents/skills/graphify/SKILL.md`, and
|
||||||
|
print a `git add` hint for files that can be committed.
|
||||||
|
Per-platform commands that support project-scoped installs accept the same flag,
|
||||||
|
for example `graphify claude install --project` or `graphify codex install --project`.
|
||||||
|
|
||||||
> **PowerShell note:** Use `graphify .` not `/graphify .` — the leading slash is a path separator in PowerShell.
|
> **PowerShell note:** Use `graphify .` not `/graphify .` — the leading slash is a path separator in PowerShell.
|
||||||
|
|
||||||
> **`graphify: command not found`?** Use `uv tool install graphifyy` or `pipx install graphifyy` — both put the CLI on PATH automatically. With plain `pip`, add `~/.local/bin` (Linux) or `~/Library/Python/3.x/bin` (Mac) to your PATH, or run `python -m graphify`.
|
> **`graphify: command not found`?** Use `uv tool install graphifyy` or `pipx install graphifyy` — both put the CLI on PATH automatically. With plain `pip`, add `~/.local/bin` (Linux) or `~/Library/Python/3.x/bin` (Mac) to your PATH, or run `python -m graphify`.
|
||||||
@@ -433,6 +447,7 @@ graphify install # overwrites the skill file
|
|||||||
|
|
||||||
graphify uninstall # remove from all platforms in one shot
|
graphify uninstall # remove from all platforms in one shot
|
||||||
graphify uninstall --purge # also delete graphify-out/
|
graphify uninstall --purge # also delete graphify-out/
|
||||||
|
graphify uninstall --project --platform codex # remove project-scoped install files only
|
||||||
|
|
||||||
graphify hook install # post-commit + post-checkout hooks
|
graphify hook install # post-commit + post-checkout hooks
|
||||||
graphify hook uninstall
|
graphify hook uninstall
|
||||||
|
|||||||
+295
-116
@@ -67,6 +67,110 @@ def _refresh_all_version_stamps() -> None:
|
|||||||
if skill_dst.exists():
|
if skill_dst.exists():
|
||||||
vf.write_text(__version__, encoding="utf-8")
|
vf.write_text(__version__, encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
cfg = _PLATFORM_CONFIG[platform_name]
|
||||||
|
if project:
|
||||||
|
return (project_dir or Path(".")) / cfg["skill_dst"]
|
||||||
|
|
||||||
|
if platform_name in ("claude", "windows") and os.environ.get("CLAUDE_CONFIG_DIR"):
|
||||||
|
return Path(os.environ["CLAUDE_CONFIG_DIR"]) / "skills" / "graphify" / "SKILL.md"
|
||||||
|
return Path.home() / cfg["skill_dst"]
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_skill_file(platform_name: str, *, project: bool = False, project_dir: Path | None = None) -> Path:
|
||||||
|
"""Copy a packaged skill file and write its version stamp."""
|
||||||
|
skill_file = "skill.md" if platform_name == "gemini" else _PLATFORM_CONFIG[platform_name]["skill_file"]
|
||||||
|
skill_src = Path(__file__).parent / skill_file
|
||||||
|
if not skill_src.exists():
|
||||||
|
print(f"error: {skill_file} not found in package - reinstall graphify", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
skill_dst = _platform_skill_destination(platform_name, project=project, project_dir=project_dir)
|
||||||
|
skill_dst.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp_dst = skill_dst.with_suffix(skill_dst.suffix + ".tmp")
|
||||||
|
try:
|
||||||
|
shutil.copy(skill_src, tmp_dst)
|
||||||
|
os.replace(tmp_dst, skill_dst)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
tmp_dst.unlink(missing_ok=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
(skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8")
|
||||||
|
print(f" skill installed -> {skill_dst}")
|
||||||
|
return skill_dst
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_skill_file(platform_name: str, *, project: bool = False, project_dir: Path | None = None) -> bool:
|
||||||
|
"""Remove a platform skill file and its version stamp without touching other scopes."""
|
||||||
|
skill_dst = _platform_skill_destination(platform_name, project=project, project_dir=project_dir)
|
||||||
|
removed = False
|
||||||
|
if skill_dst.exists():
|
||||||
|
skill_dst.unlink()
|
||||||
|
print(f" skill removed -> {skill_dst}")
|
||||||
|
removed = True
|
||||||
|
version_file = skill_dst.parent / ".graphify_version"
|
||||||
|
if version_file.exists():
|
||||||
|
version_file.unlink()
|
||||||
|
removed = True
|
||||||
|
for d in (skill_dst.parent, skill_dst.parent.parent, skill_dst.parent.parent.parent):
|
||||||
|
try:
|
||||||
|
d.rmdir()
|
||||||
|
except OSError:
|
||||||
|
break
|
||||||
|
return removed
|
||||||
|
|
||||||
|
|
||||||
|
def _project_scope_root(path: Path, project_dir: Path) -> Path:
|
||||||
|
"""Return the top-level project artifact for a project-scoped skill path."""
|
||||||
|
try:
|
||||||
|
rel = path.relative_to(project_dir)
|
||||||
|
except ValueError:
|
||||||
|
return path
|
||||||
|
return project_dir / rel.parts[0] if rel.parts else path
|
||||||
|
|
||||||
|
|
||||||
|
def _remove_claude_skill_registration(project_dir: Path) -> None:
|
||||||
|
"""Remove the project-scoped Claude skill registration file/section."""
|
||||||
|
claude_md = project_dir / ".claude" / "CLAUDE.md"
|
||||||
|
if not claude_md.exists():
|
||||||
|
return
|
||||||
|
content = claude_md.read_text(encoding="utf-8")
|
||||||
|
if "# graphify" not in content:
|
||||||
|
return
|
||||||
|
cleaned = re.sub(r"\n*# graphify\n.*?(?=\n# |\Z)", "", content, flags=re.DOTALL).rstrip()
|
||||||
|
if cleaned:
|
||||||
|
claude_md.write_text(cleaned + "\n", encoding="utf-8")
|
||||||
|
print(f" CLAUDE.md -> graphify skill registration removed from {claude_md}")
|
||||||
|
else:
|
||||||
|
claude_md.unlink()
|
||||||
|
print(f" CLAUDE.md -> deleted {claude_md}")
|
||||||
|
|
||||||
|
|
||||||
|
def _print_project_git_add_hint(paths: list[Path]) -> None:
|
||||||
|
unique: list[str] = []
|
||||||
|
for path in paths:
|
||||||
|
text = path.as_posix().rstrip("/")
|
||||||
|
if path.exists() and path.is_dir():
|
||||||
|
text += "/"
|
||||||
|
if text not in unique:
|
||||||
|
unique.append(text)
|
||||||
|
if not unique:
|
||||||
|
return
|
||||||
|
print()
|
||||||
|
print("Project-scoped install. Add to version control:")
|
||||||
|
print(f" git add {' '.join(unique)}")
|
||||||
|
|
||||||
_SETTINGS_HOOK = {
|
_SETTINGS_HOOK = {
|
||||||
# Claude Code v2.1.117+ removed dedicated Grep/Glob tools; searches now go through Bash.
|
# Claude Code v2.1.117+ removed dedicated Grep/Glob tools; searches now go through Bash.
|
||||||
# We match on Bash and inspect the command string to avoid firing on every shell call.
|
# We match on Bash and inspect the command string to avoid firing on every shell call.
|
||||||
@@ -89,13 +193,14 @@ _SETTINGS_HOOK = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
_SKILL_REGISTRATION = (
|
def _skill_registration(skill_path: str = "~/.claude/skills/graphify/SKILL.md") -> str:
|
||||||
"\n# graphify\n"
|
return (
|
||||||
"- **graphify** (`~/.claude/skills/graphify/SKILL.md`) "
|
"\n# graphify\n"
|
||||||
"- any input to knowledge graph. Trigger: `/graphify`\n"
|
f"- **graphify** (`{skill_path}`) "
|
||||||
"When the user types `/graphify`, invoke the Skill tool "
|
"- any input to knowledge graph. Trigger: `/graphify`\n"
|
||||||
"with `skill: \"graphify\"` before doing anything else.\n"
|
"When the user types `/graphify`, invoke the Skill tool "
|
||||||
)
|
"with `skill: \"graphify\"` before doing anything else.\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
_PLATFORM_CONFIG: dict[str, dict] = {
|
_PLATFORM_CONFIG: dict[str, dict] = {
|
||||||
@@ -227,9 +332,9 @@ def _replace_or_append_section(content: str, marker: str, new_section: str) -> s
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def install(platform: str = "claude") -> None:
|
def install(platform: str = "claude", *, project: bool = False, project_dir: Path | None = None) -> None:
|
||||||
if platform == "gemini":
|
if platform == "gemini":
|
||||||
gemini_install()
|
gemini_install(project_dir=project_dir, project=project)
|
||||||
return
|
return
|
||||||
if platform == "cursor":
|
if platform == "cursor":
|
||||||
_cursor_install(Path("."))
|
_cursor_install(Path("."))
|
||||||
@@ -245,52 +350,34 @@ def install(platform: str = "claude") -> None:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
cfg = _PLATFORM_CONFIG[platform]
|
cfg = _PLATFORM_CONFIG[platform]
|
||||||
skill_src = Path(__file__).parent / cfg["skill_file"]
|
project_dir = project_dir or Path(".")
|
||||||
if not skill_src.exists():
|
skill_dst = _copy_skill_file(platform, project=project, project_dir=project_dir)
|
||||||
print(f"error: {cfg['skill_file']} not found in package - reinstall graphify", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
import os as _os
|
|
||||||
if platform in ("claude", "windows") and _os.environ.get("CLAUDE_CONFIG_DIR"):
|
|
||||||
_claude_base = Path(_os.environ["CLAUDE_CONFIG_DIR"])
|
|
||||||
skill_dst = _claude_base / "skills" / "graphify" / "SKILL.md"
|
|
||||||
else:
|
|
||||||
skill_dst = Path.home() / cfg["skill_dst"]
|
|
||||||
skill_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
tmp_dst = skill_dst.with_suffix(skill_dst.suffix + ".tmp")
|
|
||||||
try:
|
|
||||||
shutil.copy(skill_src, tmp_dst)
|
|
||||||
os.replace(tmp_dst, skill_dst)
|
|
||||||
except Exception:
|
|
||||||
try:
|
|
||||||
tmp_dst.unlink(missing_ok=True)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
raise
|
|
||||||
(skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8")
|
|
||||||
print(f" skill installed -> {skill_dst}")
|
|
||||||
|
|
||||||
if cfg["claude_md"]:
|
if cfg["claude_md"]:
|
||||||
# Register in ~/.claude/CLAUDE.md (Claude Code only)
|
# Register in the matching Claude Code scope.
|
||||||
claude_md = Path.home() / ".claude" / "CLAUDE.md"
|
claude_md = (project_dir / ".claude" / "CLAUDE.md") if project else Path.home() / ".claude" / "CLAUDE.md"
|
||||||
|
registration = _skill_registration(".claude/skills/graphify/SKILL.md" if project else "~/.claude/skills/graphify/SKILL.md")
|
||||||
if claude_md.exists():
|
if claude_md.exists():
|
||||||
content = claude_md.read_text(encoding="utf-8")
|
content = claude_md.read_text(encoding="utf-8")
|
||||||
if "graphify" in content:
|
if "graphify" in content:
|
||||||
print(f" CLAUDE.md -> already registered (no change)")
|
print(f" CLAUDE.md -> already registered (no change)")
|
||||||
else:
|
else:
|
||||||
claude_md.write_text(content.rstrip() + _SKILL_REGISTRATION, encoding="utf-8")
|
claude_md.write_text(content.rstrip() + registration, encoding="utf-8")
|
||||||
print(f" CLAUDE.md -> skill registered in {claude_md}")
|
print(f" CLAUDE.md -> skill registered in {claude_md}")
|
||||||
else:
|
else:
|
||||||
claude_md.parent.mkdir(parents=True, exist_ok=True)
|
claude_md.parent.mkdir(parents=True, exist_ok=True)
|
||||||
claude_md.write_text(_SKILL_REGISTRATION.lstrip(), encoding="utf-8")
|
claude_md.write_text(registration.lstrip(), encoding="utf-8")
|
||||||
print(f" CLAUDE.md -> created at {claude_md}")
|
print(f" CLAUDE.md -> created at {claude_md}")
|
||||||
|
|
||||||
if platform == "opencode":
|
if platform == "opencode":
|
||||||
_install_opencode_plugin(Path("."))
|
_install_opencode_plugin(project_dir if project else Path("."))
|
||||||
|
|
||||||
# Refresh version stamps in all other previously-installed skill dirs so
|
# Refresh version stamps in all other previously-installed skill dirs so
|
||||||
# stale-version warnings don't fire for platforms not explicitly re-installed.
|
# stale-version warnings don't fire for platforms not explicitly re-installed.
|
||||||
_refresh_all_version_stamps()
|
if project:
|
||||||
|
_print_project_git_add_hint([_project_scope_root(skill_dst, project_dir)])
|
||||||
|
else:
|
||||||
|
_refresh_all_version_stamps()
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("Done. Open your AI coding assistant and type:")
|
print("Done. Open your AI coding assistant and type:")
|
||||||
@@ -301,7 +388,7 @@ def install(platform: str = "claude") -> None:
|
|||||||
|
|
||||||
def _print_install_usage() -> None:
|
def _print_install_usage() -> None:
|
||||||
platforms = ", ".join([*_PLATFORM_CONFIG, "gemini", "cursor"])
|
platforms = ", ".join([*_PLATFORM_CONFIG, "gemini", "cursor"])
|
||||||
print("Usage: graphify install [--platform P|P]")
|
print("Usage: graphify install [--project] [--platform P|P]")
|
||||||
print(f"Platforms: {platforms}")
|
print(f"Platforms: {platforms}")
|
||||||
|
|
||||||
|
|
||||||
@@ -371,21 +458,12 @@ _GEMINI_HOOK = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def gemini_install(project_dir: Path | None = None) -> None:
|
def gemini_install(project_dir: Path | None = None, *, project: bool = False) -> None:
|
||||||
"""Copy skill file to ~/.gemini/skills/graphify/, write GEMINI.md section, and install BeforeTool hook."""
|
"""Copy skill file, write GEMINI.md section, and install BeforeTool hook."""
|
||||||
# Copy skill file to ~/.gemini/skills/graphify/SKILL.md
|
project_dir = project_dir or Path(".")
|
||||||
# On Windows, Gemini CLI prioritises ~/.agents/skills/ over ~/.gemini/skills/
|
skill_dst = _copy_skill_file("gemini", project=project, project_dir=project_dir)
|
||||||
skill_src = Path(__file__).parent / "skill.md"
|
|
||||||
if platform.system() == "Windows":
|
|
||||||
skill_dst = Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md"
|
|
||||||
else:
|
|
||||||
skill_dst = Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
|
|
||||||
skill_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
shutil.copy(skill_src, skill_dst)
|
|
||||||
(skill_dst.parent / ".graphify_version").write_text(__version__, encoding="utf-8")
|
|
||||||
print(f" skill installed -> {skill_dst}")
|
|
||||||
|
|
||||||
target = (project_dir or Path(".")) / "GEMINI.md"
|
target = project_dir / "GEMINI.md"
|
||||||
|
|
||||||
if target.exists():
|
if target.exists():
|
||||||
content = target.read_text(encoding="utf-8")
|
content = target.read_text(encoding="utf-8")
|
||||||
@@ -403,7 +481,9 @@ def gemini_install(project_dir: Path | None = None) -> None:
|
|||||||
|
|
||||||
# Always re-install the Gemini hook so an older payload (e.g. pre-issue-#580
|
# Always re-install the Gemini hook so an older payload (e.g. pre-issue-#580
|
||||||
# wording) is replaced on upgrade.
|
# wording) is replaced on upgrade.
|
||||||
_install_gemini_hook(project_dir or Path("."))
|
_install_gemini_hook(project_dir)
|
||||||
|
if project:
|
||||||
|
_print_project_git_add_hint([_project_scope_root(skill_dst, project_dir), project_dir / "GEMINI.md", project_dir / ".gemini"])
|
||||||
print()
|
print()
|
||||||
print("Gemini CLI will now check the knowledge graph before answering")
|
print("Gemini CLI will now check the knowledge graph before answering")
|
||||||
print("codebase questions and rebuild it after code changes.")
|
print("codebase questions and rebuild it after code changes.")
|
||||||
@@ -440,26 +520,12 @@ def _uninstall_gemini_hook(project_dir: Path) -> None:
|
|||||||
print(" .gemini/settings.json -> BeforeTool hook removed")
|
print(" .gemini/settings.json -> BeforeTool hook removed")
|
||||||
|
|
||||||
|
|
||||||
def gemini_uninstall(project_dir: Path | None = None) -> None:
|
def gemini_uninstall(project_dir: Path | None = None, *, project: bool = False) -> None:
|
||||||
"""Remove the graphify section from GEMINI.md, uninstall hook, and remove skill file."""
|
"""Remove the graphify section from GEMINI.md, uninstall hook, and remove skill file."""
|
||||||
# Remove skill file (mirror the install path detection)
|
project_dir = project_dir or Path(".")
|
||||||
if platform.system() == "Windows":
|
_remove_skill_file("gemini", project=project, project_dir=project_dir)
|
||||||
skill_dst = Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md"
|
|
||||||
else:
|
|
||||||
skill_dst = Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
|
|
||||||
if skill_dst.exists():
|
|
||||||
skill_dst.unlink()
|
|
||||||
print(f" skill removed -> {skill_dst}")
|
|
||||||
version_file = skill_dst.parent / ".graphify_version"
|
|
||||||
if version_file.exists():
|
|
||||||
version_file.unlink()
|
|
||||||
for d in (skill_dst.parent, skill_dst.parent.parent):
|
|
||||||
try:
|
|
||||||
d.rmdir()
|
|
||||||
except OSError:
|
|
||||||
break
|
|
||||||
|
|
||||||
target = (project_dir or Path(".")) / "GEMINI.md"
|
target = project_dir / "GEMINI.md"
|
||||||
if not target.exists():
|
if not target.exists():
|
||||||
print("No GEMINI.md found in current directory - nothing to do")
|
print("No GEMINI.md found in current directory - nothing to do")
|
||||||
return
|
return
|
||||||
@@ -474,7 +540,7 @@ def gemini_uninstall(project_dir: Path | None = None) -> None:
|
|||||||
else:
|
else:
|
||||||
target.unlink()
|
target.unlink()
|
||||||
print(f"GEMINI.md was empty after removal - deleted {target.resolve()}")
|
print(f"GEMINI.md was empty after removal - deleted {target.resolve()}")
|
||||||
_uninstall_gemini_hook(project_dir or Path("."))
|
_uninstall_gemini_hook(project_dir)
|
||||||
|
|
||||||
|
|
||||||
_VSCODE_INSTRUCTIONS_MARKER = "## graphify"
|
_VSCODE_INSTRUCTIONS_MARKER = "## graphify"
|
||||||
@@ -990,6 +1056,76 @@ def _agents_install(project_dir: Path, platform: str) -> None:
|
|||||||
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:
|
||||||
|
"""Install platform skill/config files in the current project."""
|
||||||
|
project_dir = project_dir or Path(".")
|
||||||
|
if platform_name in ("claude", "windows"):
|
||||||
|
install(platform=platform_name, project=True, project_dir=project_dir)
|
||||||
|
claude_install(project_dir)
|
||||||
|
_print_project_git_add_hint([project_dir / ".claude", project_dir / "CLAUDE.md"])
|
||||||
|
elif platform_name == "gemini":
|
||||||
|
gemini_install(project_dir, project=True)
|
||||||
|
elif platform_name == "cursor":
|
||||||
|
_cursor_install(project_dir)
|
||||||
|
_print_project_git_add_hint([project_dir / ".cursor"])
|
||||||
|
elif platform_name == "kiro":
|
||||||
|
_kiro_install(project_dir)
|
||||||
|
_print_project_git_add_hint([project_dir / ".kiro"])
|
||||||
|
elif platform_name in ("aider", "codex", "opencode", "claw", "droid", "trae", "trae-cn", "hermes"):
|
||||||
|
skill_dst = _copy_skill_file(platform_name, project=True, project_dir=project_dir)
|
||||||
|
_agents_install(project_dir, platform_name)
|
||||||
|
hint_paths = [_project_scope_root(skill_dst, project_dir), project_dir / "AGENTS.md"]
|
||||||
|
if platform_name == "opencode":
|
||||||
|
hint_paths.append(project_dir / ".opencode")
|
||||||
|
elif platform_name == "codex":
|
||||||
|
hint_paths.append(project_dir / ".codex")
|
||||||
|
_print_project_git_add_hint(hint_paths)
|
||||||
|
elif platform_name in ("copilot", "pi", "antigravity", "kimi"):
|
||||||
|
skill_dst = _copy_skill_file(platform_name, project=True, project_dir=project_dir)
|
||||||
|
_print_project_git_add_hint([_project_scope_root(skill_dst, project_dir)])
|
||||||
|
else:
|
||||||
|
install(platform=platform_name, project=True, project_dir=project_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_uninstall(platform_name: str, project_dir: Path | None = None) -> None:
|
||||||
|
"""Remove project-scoped platform skill/config files only."""
|
||||||
|
project_dir = project_dir or Path(".")
|
||||||
|
if platform_name in ("claude", "windows"):
|
||||||
|
_remove_skill_file(platform_name, project=True, project_dir=project_dir)
|
||||||
|
_remove_claude_skill_registration(project_dir)
|
||||||
|
claude_uninstall(project_dir)
|
||||||
|
elif platform_name == "gemini":
|
||||||
|
gemini_uninstall(project_dir, project=True)
|
||||||
|
elif platform_name == "cursor":
|
||||||
|
_cursor_uninstall(project_dir)
|
||||||
|
elif platform_name == "kiro":
|
||||||
|
_kiro_uninstall(project_dir)
|
||||||
|
elif platform_name in ("aider", "codex", "opencode", "claw", "droid", "trae", "trae-cn", "hermes"):
|
||||||
|
_remove_skill_file(platform_name, project=True, project_dir=project_dir)
|
||||||
|
_agents_uninstall(project_dir, platform=platform_name)
|
||||||
|
if platform_name == "codex":
|
||||||
|
_uninstall_codex_hook(project_dir)
|
||||||
|
elif platform_name == "antigravity":
|
||||||
|
_antigravity_uninstall(project_dir)
|
||||||
|
elif platform_name in ("copilot", "pi", "kimi"):
|
||||||
|
removed = _remove_skill_file(platform_name, project=True, project_dir=project_dir)
|
||||||
|
if not removed:
|
||||||
|
print("nothing to remove")
|
||||||
|
else:
|
||||||
|
_remove_skill_file(platform_name, project=True, project_dir=project_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _project_uninstall_all(project_dir: Path | None = None) -> None:
|
||||||
|
"""Remove project-scoped install files without touching user-scope installs."""
|
||||||
|
project_dir = project_dir or Path(".")
|
||||||
|
print("Uninstalling project-scoped graphify files...\n")
|
||||||
|
for platform_name in _PLATFORM_CONFIG:
|
||||||
|
_project_uninstall(platform_name, project_dir)
|
||||||
|
for platform_name in ("gemini", "cursor"):
|
||||||
|
_project_uninstall(platform_name, project_dir)
|
||||||
|
print("\nDone.")
|
||||||
|
|
||||||
|
|
||||||
def _agents_uninstall(project_dir: Path, platform: str = "") -> None:
|
def _agents_uninstall(project_dir: Path, platform: str = "") -> None:
|
||||||
"""Remove the graphify section from the local AGENTS.md."""
|
"""Remove the graphify section from the local AGENTS.md."""
|
||||||
target = (project_dir or Path(".")) / "AGENTS.md"
|
target = (project_dir or Path(".")) / "AGENTS.md"
|
||||||
@@ -1361,6 +1497,7 @@ def main() -> None:
|
|||||||
# Default to windows platform on Windows, claude elsewhere
|
# Default to windows platform on Windows, claude elsewhere
|
||||||
default_platform = "windows" if platform.system() == "Windows" else "claude"
|
default_platform = "windows" if platform.system() == "Windows" else "claude"
|
||||||
selected_platform: str | None = None
|
selected_platform: str | None = None
|
||||||
|
project_scope = False
|
||||||
args = sys.argv[2:]
|
args = sys.argv[2:]
|
||||||
i = 0
|
i = 0
|
||||||
while i < len(args):
|
while i < len(args):
|
||||||
@@ -1368,7 +1505,10 @@ def main() -> None:
|
|||||||
if arg in ("-h", "--help"):
|
if arg in ("-h", "--help"):
|
||||||
_print_install_usage()
|
_print_install_usage()
|
||||||
return
|
return
|
||||||
if arg.startswith("--platform="):
|
if arg == "--project":
|
||||||
|
project_scope = True
|
||||||
|
i += 1
|
||||||
|
elif arg.startswith("--platform="):
|
||||||
candidate = arg.split("=", 1)[1]
|
candidate = arg.split("=", 1)[1]
|
||||||
if selected_platform and selected_platform != candidate:
|
if selected_platform and selected_platform != candidate:
|
||||||
print("error: specify install platform only once", file=sys.stderr)
|
print("error: specify install platform only once", file=sys.stderr)
|
||||||
@@ -1395,25 +1535,63 @@ def main() -> None:
|
|||||||
selected_platform = arg
|
selected_platform = arg
|
||||||
i += 1
|
i += 1
|
||||||
chosen_platform = selected_platform or default_platform
|
chosen_platform = selected_platform or default_platform
|
||||||
install(platform=chosen_platform)
|
if project_scope:
|
||||||
|
_project_install(chosen_platform, Path("."))
|
||||||
|
else:
|
||||||
|
install(platform=chosen_platform)
|
||||||
elif cmd == "uninstall":
|
elif cmd == "uninstall":
|
||||||
purge = "--purge" in sys.argv[2:]
|
args = sys.argv[2:]
|
||||||
uninstall_all(purge=purge)
|
purge = "--purge" in args
|
||||||
|
project_scope = "--project" in args
|
||||||
|
selected_platform = None
|
||||||
|
i = 0
|
||||||
|
while i < len(args):
|
||||||
|
arg = args[i]
|
||||||
|
if arg in ("--purge", "--project"):
|
||||||
|
i += 1
|
||||||
|
elif arg.startswith("--platform="):
|
||||||
|
selected_platform = arg.split("=", 1)[1]
|
||||||
|
i += 1
|
||||||
|
elif arg == "--platform":
|
||||||
|
if i + 1 >= len(args):
|
||||||
|
print("error: --platform requires a value", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
selected_platform = args[i + 1]
|
||||||
|
i += 2
|
||||||
|
elif arg.startswith("-"):
|
||||||
|
print(f"error: unknown uninstall option '{arg}'", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
selected_platform = arg
|
||||||
|
i += 1
|
||||||
|
if project_scope:
|
||||||
|
if selected_platform:
|
||||||
|
_project_uninstall(selected_platform, Path("."))
|
||||||
|
else:
|
||||||
|
_project_uninstall_all(Path("."))
|
||||||
|
else:
|
||||||
|
uninstall_all(purge=purge)
|
||||||
elif cmd == "claude":
|
elif cmd == "claude":
|
||||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||||
if subcmd == "install":
|
if subcmd == "install":
|
||||||
claude_install()
|
if "--project" in sys.argv[3:]:
|
||||||
|
_project_install("claude", Path("."))
|
||||||
|
else:
|
||||||
|
claude_install()
|
||||||
elif subcmd == "uninstall":
|
elif subcmd == "uninstall":
|
||||||
claude_uninstall()
|
if "--project" in sys.argv[3:]:
|
||||||
|
_project_uninstall("claude", Path("."))
|
||||||
|
else:
|
||||||
|
claude_uninstall()
|
||||||
else:
|
else:
|
||||||
print("Usage: graphify claude [install|uninstall]", file=sys.stderr)
|
print("Usage: graphify claude [install|uninstall]", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif cmd == "gemini":
|
elif cmd == "gemini":
|
||||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||||
if subcmd == "install":
|
if subcmd == "install":
|
||||||
gemini_install()
|
gemini_install(project=("--project" in sys.argv[3:]))
|
||||||
elif subcmd == "uninstall":
|
elif subcmd == "uninstall":
|
||||||
gemini_uninstall()
|
gemini_uninstall(project=("--project" in sys.argv[3:]))
|
||||||
else:
|
else:
|
||||||
print("Usage: graphify gemini [install|uninstall]", file=sys.stderr)
|
print("Usage: graphify gemini [install|uninstall]", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1438,22 +1616,16 @@ def main() -> None:
|
|||||||
elif cmd == "copilot":
|
elif cmd == "copilot":
|
||||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||||
if subcmd == "install":
|
if subcmd == "install":
|
||||||
install(platform="copilot")
|
if "--project" in sys.argv[3:]:
|
||||||
|
_project_install("copilot", Path("."))
|
||||||
|
else:
|
||||||
|
install(platform="copilot")
|
||||||
elif subcmd == "uninstall":
|
elif subcmd == "uninstall":
|
||||||
skill_dst = Path.home() / _PLATFORM_CONFIG["copilot"]["skill_dst"]
|
if "--project" in sys.argv[3:]:
|
||||||
removed = []
|
_project_uninstall("copilot", Path("."))
|
||||||
if skill_dst.exists():
|
else:
|
||||||
skill_dst.unlink()
|
removed = _remove_skill_file("copilot")
|
||||||
removed.append(f"skill removed: {skill_dst}")
|
print("skill removed" if removed else "nothing to remove")
|
||||||
version_file = skill_dst.parent / ".graphify_version"
|
|
||||||
if version_file.exists():
|
|
||||||
version_file.unlink()
|
|
||||||
for d in (skill_dst.parent, skill_dst.parent.parent, skill_dst.parent.parent.parent):
|
|
||||||
try:
|
|
||||||
d.rmdir()
|
|
||||||
except OSError:
|
|
||||||
break
|
|
||||||
print("; ".join(removed) if removed else "nothing to remove")
|
|
||||||
else:
|
else:
|
||||||
print("Usage: graphify copilot [install|uninstall]", file=sys.stderr)
|
print("Usage: graphify copilot [install|uninstall]", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
@@ -1469,40 +1641,47 @@ def main() -> None:
|
|||||||
elif cmd == "pi":
|
elif cmd == "pi":
|
||||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||||
if subcmd == "install":
|
if subcmd == "install":
|
||||||
install("pi")
|
if "--project" in sys.argv[3:]:
|
||||||
|
_project_install("pi", Path("."))
|
||||||
|
else:
|
||||||
|
install("pi")
|
||||||
elif subcmd == "uninstall":
|
elif subcmd == "uninstall":
|
||||||
skill_dst = Path.home() / ".pi" / "agent" / "skills" / "graphify" / "SKILL.md"
|
if "--project" in sys.argv[3:]:
|
||||||
if skill_dst.exists():
|
_project_uninstall("pi", Path("."))
|
||||||
skill_dst.unlink()
|
else:
|
||||||
print(f" skill removed -> {skill_dst}")
|
_remove_skill_file("pi")
|
||||||
version_file = skill_dst.parent / ".graphify_version"
|
|
||||||
if version_file.exists():
|
|
||||||
version_file.unlink()
|
|
||||||
for d in (skill_dst.parent, skill_dst.parent.parent, skill_dst.parent.parent.parent):
|
|
||||||
try:
|
|
||||||
d.rmdir()
|
|
||||||
except OSError:
|
|
||||||
break
|
|
||||||
else:
|
else:
|
||||||
print("Usage: graphify pi [install|uninstall]", file=sys.stderr)
|
print("Usage: graphify pi [install|uninstall]", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif cmd in ("aider", "codex", "opencode", "claw", "droid", "trae", "trae-cn", "hermes"):
|
elif cmd in ("aider", "codex", "opencode", "claw", "droid", "trae", "trae-cn", "hermes"):
|
||||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||||
if subcmd == "install":
|
if subcmd == "install":
|
||||||
_agents_install(Path("."), cmd)
|
if "--project" in sys.argv[3:]:
|
||||||
|
_project_install(cmd, Path("."))
|
||||||
|
else:
|
||||||
|
_agents_install(Path("."), cmd)
|
||||||
elif subcmd == "uninstall":
|
elif subcmd == "uninstall":
|
||||||
_agents_uninstall(Path("."), platform=cmd)
|
if "--project" in sys.argv[3:]:
|
||||||
if cmd == "codex":
|
_project_uninstall(cmd, Path("."))
|
||||||
_uninstall_codex_hook(Path("."))
|
else:
|
||||||
|
_agents_uninstall(Path("."), platform=cmd)
|
||||||
|
if cmd == "codex":
|
||||||
|
_uninstall_codex_hook(Path("."))
|
||||||
else:
|
else:
|
||||||
print(f"Usage: graphify {cmd} [install|uninstall]", file=sys.stderr)
|
print(f"Usage: graphify {cmd} [install|uninstall]", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
elif cmd == "antigravity":
|
elif cmd == "antigravity":
|
||||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||||
if subcmd == "install":
|
if subcmd == "install":
|
||||||
_antigravity_install(Path("."))
|
if "--project" in sys.argv[3:]:
|
||||||
|
_project_install("antigravity", Path("."))
|
||||||
|
else:
|
||||||
|
_antigravity_install(Path("."))
|
||||||
elif subcmd == "uninstall":
|
elif subcmd == "uninstall":
|
||||||
_antigravity_uninstall(Path("."))
|
if "--project" in sys.argv[3:]:
|
||||||
|
_project_uninstall("antigravity", Path("."))
|
||||||
|
else:
|
||||||
|
_antigravity_uninstall(Path("."))
|
||||||
else:
|
else:
|
||||||
print("Usage: graphify antigravity [install|uninstall]", file=sys.stderr)
|
print("Usage: graphify antigravity [install|uninstall]", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
@@ -54,6 +54,105 @@ def test_install_positional_platform_opencode(tmp_path, monkeypatch):
|
|||||||
assert not (tmp_path / ".claude" / "skills" / "graphify" / "SKILL.md").exists()
|
assert not (tmp_path / ".claude" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_project_claude_writes_project_scope(tmp_path, monkeypatch, capsys):
|
||||||
|
from graphify.__main__ import main
|
||||||
|
home = tmp_path / "home"
|
||||||
|
project = tmp_path / "project"
|
||||||
|
project.mkdir()
|
||||||
|
monkeypatch.chdir(project)
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "install", "--project"])
|
||||||
|
with patch("graphify.__main__.Path.home", return_value=home):
|
||||||
|
main()
|
||||||
|
assert (project / ".claude" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
assert (project / ".claude" / "CLAUDE.md").exists()
|
||||||
|
assert not (home / ".claude" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
assert ".claude/skills/graphify/SKILL.md" in (project / ".claude" / "CLAUDE.md").read_text()
|
||||||
|
assert "~/.claude/skills/graphify/SKILL.md" not in (project / ".claude" / "CLAUDE.md").read_text()
|
||||||
|
assert "git add .claude/" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_project_codex_writes_skill_and_agents(tmp_path, monkeypatch):
|
||||||
|
from graphify.__main__ import main
|
||||||
|
home = tmp_path / "home"
|
||||||
|
project = tmp_path / "project"
|
||||||
|
project.mkdir()
|
||||||
|
monkeypatch.chdir(project)
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "install", "--project", "--platform", "codex"])
|
||||||
|
with patch("graphify.__main__.Path.home", return_value=home):
|
||||||
|
main()
|
||||||
|
assert (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
assert (project / "AGENTS.md").exists()
|
||||||
|
assert (project / ".codex" / "hooks.json").exists()
|
||||||
|
assert not (home / ".agents" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_claude_subcommand_project_install_and_uninstall_are_project_scoped(tmp_path, monkeypatch):
|
||||||
|
from graphify.__main__ import main
|
||||||
|
home = tmp_path / "home"
|
||||||
|
project = tmp_path / "project"
|
||||||
|
project.mkdir()
|
||||||
|
user_skill = home / ".claude" / "skills" / "graphify" / "SKILL.md"
|
||||||
|
user_skill.parent.mkdir(parents=True)
|
||||||
|
user_skill.write_text("user skill")
|
||||||
|
monkeypatch.chdir(project)
|
||||||
|
with patch("graphify.__main__.Path.home", return_value=home):
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "claude", "install", "--project"])
|
||||||
|
main()
|
||||||
|
assert (project / ".claude" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
assert (project / ".claude" / "CLAUDE.md").exists()
|
||||||
|
assert (project / "CLAUDE.md").exists()
|
||||||
|
assert user_skill.exists()
|
||||||
|
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "claude", "uninstall", "--project"])
|
||||||
|
main()
|
||||||
|
|
||||||
|
assert user_skill.exists()
|
||||||
|
assert not (project / ".claude" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
assert not (project / ".claude" / "CLAUDE.md").exists()
|
||||||
|
assert not (project / "CLAUDE.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_codex_subcommand_project_install_and_uninstall_are_project_scoped(tmp_path, monkeypatch):
|
||||||
|
from graphify.__main__ import main
|
||||||
|
home = tmp_path / "home"
|
||||||
|
project = tmp_path / "project"
|
||||||
|
project.mkdir()
|
||||||
|
user_skill = home / ".agents" / "skills" / "graphify" / "SKILL.md"
|
||||||
|
user_skill.parent.mkdir(parents=True)
|
||||||
|
user_skill.write_text("user skill")
|
||||||
|
monkeypatch.chdir(project)
|
||||||
|
with patch("graphify.__main__.Path.home", return_value=home):
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "codex", "install", "--project"])
|
||||||
|
main()
|
||||||
|
assert (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
assert (project / "AGENTS.md").exists()
|
||||||
|
assert (project / ".codex" / "hooks.json").exists()
|
||||||
|
assert user_skill.exists()
|
||||||
|
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "codex", "uninstall", "--project"])
|
||||||
|
main()
|
||||||
|
|
||||||
|
assert user_skill.exists()
|
||||||
|
assert not (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
assert not (project / "AGENTS.md").exists()
|
||||||
|
hooks_path = project / ".codex" / "hooks.json"
|
||||||
|
assert hooks_path.exists()
|
||||||
|
assert "graphify" not in hooks_path.read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_antigravity_install_project_writes_project_skill(tmp_path, monkeypatch):
|
||||||
|
from graphify.__main__ import main
|
||||||
|
home = tmp_path / "home"
|
||||||
|
project = tmp_path / "project"
|
||||||
|
project.mkdir()
|
||||||
|
monkeypatch.chdir(project)
|
||||||
|
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()
|
||||||
|
|
||||||
|
|
||||||
def test_install_help_does_not_install_default(tmp_path, monkeypatch, capsys):
|
def test_install_help_does_not_install_default(tmp_path, monkeypatch, capsys):
|
||||||
from graphify.__main__ import main
|
from graphify.__main__ import main
|
||||||
monkeypatch.chdir(tmp_path)
|
monkeypatch.chdir(tmp_path)
|
||||||
@@ -171,6 +270,62 @@ def test_codex_install_does_not_write_claude_md(tmp_path):
|
|||||||
assert not (tmp_path / ".claude" / "CLAUDE.md").exists()
|
assert not (tmp_path / ".claude" / "CLAUDE.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_uninstall_project_removes_project_skill_only(tmp_path, monkeypatch):
|
||||||
|
from graphify.__main__ import main
|
||||||
|
home = tmp_path / "home"
|
||||||
|
project = tmp_path / "project"
|
||||||
|
project.mkdir()
|
||||||
|
user_skill = home / ".agents" / "skills" / "graphify" / "SKILL.md"
|
||||||
|
user_skill.parent.mkdir(parents=True)
|
||||||
|
user_skill.write_text("user skill")
|
||||||
|
monkeypatch.chdir(project)
|
||||||
|
with patch("graphify.__main__.Path.home", return_value=home):
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "install", "--project", "--platform", "codex"])
|
||||||
|
main()
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "uninstall", "--project", "--platform", "codex"])
|
||||||
|
main()
|
||||||
|
assert user_skill.exists()
|
||||||
|
assert not (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
assert not (project / "AGENTS.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_uninstall_project_without_platform_removes_project_installs(tmp_path, monkeypatch):
|
||||||
|
from graphify.__main__ import main
|
||||||
|
home = tmp_path / "home"
|
||||||
|
project = tmp_path / "project"
|
||||||
|
project.mkdir()
|
||||||
|
user_skill = home / ".claude" / "skills" / "graphify" / "SKILL.md"
|
||||||
|
user_skill.parent.mkdir(parents=True)
|
||||||
|
user_skill.write_text("user skill")
|
||||||
|
monkeypatch.chdir(project)
|
||||||
|
with patch("graphify.__main__.Path.home", return_value=home):
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "install", "--project"])
|
||||||
|
main()
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "uninstall", "--project"])
|
||||||
|
main()
|
||||||
|
assert user_skill.exists()
|
||||||
|
assert not (project / ".claude" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
assert not (project / ".claude" / "CLAUDE.md").exists()
|
||||||
|
|
||||||
|
|
||||||
|
def test_antigravity_uninstall_project_removes_project_skill_only(tmp_path, monkeypatch):
|
||||||
|
from graphify.__main__ import main
|
||||||
|
home = tmp_path / "home"
|
||||||
|
project = tmp_path / "project"
|
||||||
|
project.mkdir()
|
||||||
|
user_skill = home / ".agents" / "skills" / "graphify" / "SKILL.md"
|
||||||
|
user_skill.parent.mkdir(parents=True)
|
||||||
|
user_skill.write_text("user skill")
|
||||||
|
monkeypatch.chdir(project)
|
||||||
|
with patch("graphify.__main__.Path.home", return_value=home):
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "antigravity", "install", "--project"])
|
||||||
|
main()
|
||||||
|
monkeypatch.setattr(sys, "argv", ["graphify", "antigravity", "uninstall", "--project"])
|
||||||
|
main()
|
||||||
|
assert user_skill.exists()
|
||||||
|
assert not (project / ".agents" / "skills" / "graphify" / "SKILL.md").exists()
|
||||||
|
|
||||||
|
|
||||||
# --- always-on AGENTS.md install/uninstall tests ---
|
# --- always-on AGENTS.md install/uninstall tests ---
|
||||||
|
|
||||||
def _agents_install(tmp_path, platform):
|
def _agents_install(tmp_path, platform):
|
||||||
|
|||||||
Reference in New Issue
Block a user