From 1bd22994e024bca6f69ad7e27e92ebb05874e55f Mon Sep 17 00:00:00 2001 From: Andre Brait Date: Thu, 24 Sep 2026 03:31:55 +0000 Subject: [PATCH] feat(install): add the omp platform for Oh My Pi skill installs OMP reads its agent skills from ~/.omp/agent/skills, not the legacy ~/.pi path the pi platform writes, so the installer had no OMP target even though 'omp' was already reserved in the CLI command set. Adds the omp platform reusing pi's skill bundle (OMP mirrors pi's agent layout), the pi-twin 'graphify omp' subcommand, and the project-scope groups; tests lock destinations, bundle reuse, install/uninstall, and uninstall_all pi parity. --- graphify/install.py | 28 ++++++- tests/test_omp_platform.py | 158 +++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 tests/test_omp_platform.py diff --git a/graphify/install.py b/graphify/install.py index 0001daa4dd..18290e93d5 100644 --- a/graphify/install.py +++ b/graphify/install.py @@ -497,6 +497,14 @@ def _register_always_on_block(target: Path, prefix: str, registration: str) -> N "claude_md": False, "skill_refs": "pi", }, + "omp": { + # OMP (Oh My Pi) mirrors pi's agent layout under ~/.omp, which the + # legacy ~/.pi path is no longer read from. + "skill_file": "skill-pi.md", + "skill_dst": Path(".omp") / "agent" / "skills" / "graphify" / "SKILL.md", + "claude_md": False, + "skill_refs": "pi", + }, "codebuddy": { # Reuses claude's split bundle (shares skill.md). "skill_file": "skill.md", @@ -1701,7 +1709,7 @@ def _project_install(platform_name: str, project_dir: Path | None = None, strict skill_dst = _copy_skill_file("antigravity", project=True, project_dir=project_dir) _antigravity_finalize(skill_dst, project_dir) _print_project_git_add_hint([_project_scope_root(skill_dst, project_dir), project_dir / ".agents"]) - elif platform_name in ("copilot", "pi", "kimi", "agents"): + elif platform_name in ("copilot", "pi", "kimi", "agents", "omp"): # Skill-only project install: drop SKILL.md (+ references) at the scope # root. `agents` -> ./.agents/skills/graphify/SKILL.md. skill_dst = _copy_skill_file(platform_name, project=True, project_dir=project_dir) @@ -1734,7 +1742,7 @@ def _project_uninstall(platform_name: str, project_dir: Path | None = None) -> N _devin_rules_uninstall(project_dir) if not removed: print("nothing to remove") - elif platform_name in ("copilot", "pi", "kimi", "agents"): + elif platform_name in ("copilot", "pi", "kimi", "agents", "omp"): removed = _remove_skill_file(platform_name, project=True, project_dir=project_dir) if not removed: print("nothing to remove") @@ -2129,6 +2137,7 @@ def codebuddy_uninstall(project_dir: Path | None = None, *, project: bool = Fals "kilo", "kiro", "opencode", + "omp", "pi", "skills", "trae", @@ -2351,6 +2360,21 @@ def dispatch_install_cli(cmd: str) -> bool: else: print("Usage: graphify pi [install|uninstall]", file=sys.stderr) sys.exit(1) + elif cmd == "omp": + subcmd = sys.argv[2] if len(sys.argv) > 2 else "" + if subcmd == "install": + if "--project" in sys.argv[3:]: + _project_install("omp", Path(".")) + else: + install("omp") + elif subcmd == "uninstall": + if "--project" in sys.argv[3:]: + _project_uninstall("omp", Path(".")) + else: + _remove_skill_file("omp") + else: + print("Usage: graphify omp [install|uninstall]", file=sys.stderr) + sys.exit(1) elif cmd == "amp": subcmd = sys.argv[2] if len(sys.argv) > 2 else "" if subcmd == "install": diff --git a/tests/test_omp_platform.py b/tests/test_omp_platform.py new file mode 100644 index 0000000000..b2a06b1af8 --- /dev/null +++ b/tests/test_omp_platform.py @@ -0,0 +1,158 @@ +"""Tests for the OMP (Oh My Pi) install platform. + +`graphify install --platform omp` installs the skill into OMP's agent +directory — ``~/.omp/agent/skills`` globally, ``./.omp/agent/skills`` in +project scope — reusing pi's skill bundle, because OMP mirrors pi's agent +layout. The ``graphify omp`` subcommand mirrors the ``graphify pi`` twin. +""" +import os +import sys +from pathlib import Path +from unittest.mock import patch + +import pytest + +import graphify.__main__ as mainmod + + +# --- destination map ----------------------------------------------------------- + + +def test_omp_user_destination_is_omp_agent_skills(tmp_path): + """Global omp skill lands at ~/.omp/agent/skills — the active OMP agent + directory, NOT the legacy ~/.pi path OMP no longer reads.""" + with patch("graphify.__main__.Path.home", return_value=tmp_path): + dst = mainmod._platform_skill_destination("omp", project=False) + assert dst == tmp_path / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + + +def test_omp_project_destination_is_omp_agent_skills(tmp_path): + """Project omp skill lands at ./.omp/agent/skills.""" + dst = mainmod._platform_skill_destination("omp", project=True, project_dir=tmp_path) + assert dst == tmp_path / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + + +def test_omp_reuses_the_pi_bundle(): + """omp installs pi's skill file and references sidecar verbatim: OMP is a + pi successor with the same skill format, so a second bundle would only + drift.""" + cfg = mainmod._PLATFORM_CONFIG["omp"] + assert cfg["skill_file"] == mainmod._PLATFORM_CONFIG["pi"]["skill_file"] + assert cfg["skill_refs"] == "pi" + assert cfg["claude_md"] is False + + +# --- end-to-end install / uninstall via the CLI -------------------------------- + + +def _run(tmp_path, argv, home): + """Drive main() with argv, cwd at tmp_path, and Path.home redirected.""" + old_cwd = Path.cwd() + try: + os.chdir(tmp_path) + with patch.object(sys, "argv", ["graphify", *argv]): + with patch("graphify.__main__.Path.home", return_value=home): + mainmod.main() + finally: + os.chdir(old_cwd) + + +def test_install_platform_omp_writes_user_skill(tmp_path): + """`graphify install --platform omp` writes ~/.omp/agent/skills/... + SKILL.md (+ pi's references) and nothing else — no CLAUDE.md (skill-only).""" + home = tmp_path / "home" + cwd = tmp_path / "cwd" + home.mkdir() + cwd.mkdir() + + _run(cwd, ["install", "--platform", "omp"], home) + + skill = home / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + assert skill.exists() + assert (skill.parent / ".graphify_version").read_text() == mainmod.__version__ + assert (skill.parent / "references" / "extraction-spec.md").exists() + # Skill-only: the --platform path must not write a CLAUDE.md. + assert not (cwd / "CLAUDE.md").exists() + + +def test_uninstall_platform_flag_omp_matches_pi_parity(tmp_path): + """`graphify uninstall --platform omp` (global) is accepted. + + uninstall_all removes user-scope skills for its listed platforms; pi — + omp's bundle twin — is not among them, so omp matches pi exactly: the + flag form is accepted and the user-scope skill is removed by the + `graphify omp uninstall` subcommand (locked in below), not by + uninstall_all. Locking the pi parity stops a future edit from silently + special-casing one twin. + """ + home = tmp_path / "home" + cwd = tmp_path / "cwd" + home.mkdir() + cwd.mkdir() + + _run(cwd, ["install", "--platform", "omp"], home) + skill = home / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + assert skill.exists() + + _run(cwd, ["uninstall", "--platform", "omp"], home) + # Same state pi leaves after its global uninstall: skill intact. + assert skill.exists() + + _run(cwd, ["omp", "uninstall"], home) + assert not skill.exists() + + +def test_install_platform_omp_project_writes_omp_agent_skills(tmp_path): + """`graphify install --project --platform omp` writes ./.omp/agent/skills and + leaves user scope untouched.""" + home = tmp_path / "home" + proj = tmp_path / "proj" + home.mkdir() + proj.mkdir() + + _run(proj, ["install", "--project", "--platform", "omp"], home) + + project_skill = proj / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + assert project_skill.exists() + assert (project_skill.parent / "references" / "extraction-spec.md").exists() + # User scope was not touched. + assert not (home / ".omp" / "agent").exists() + + _run(proj, ["uninstall", "--project", "--platform", "omp"], home) + assert not project_skill.exists() + + +# --- the pi-twin subcommand (graphify omp install) ------------------------------ + + +def test_omp_subcommand_installs_same_skill(tmp_path): + """`graphify omp install` installs the identical user-global skill as + `--platform omp` — same destination, same version stamp.""" + home = tmp_path / "home" + cwd = tmp_path / "cwd" + home.mkdir() + cwd.mkdir() + + _run(cwd, ["omp", "install"], home) + + skill = home / ".omp" / "agent" / "skills" / "graphify" / "SKILL.md" + assert skill.exists() + assert (skill.parent / ".graphify_version").read_text() == mainmod.__version__ + + _run(cwd, ["omp", "uninstall"], home) + assert not skill.exists() + + +def test_unknown_platform_error_lists_omp(capsys, tmp_path): + """The unknown-platform error is built from _PLATFORM_CONFIG, so the new + platform must appear in its choices — a guard against a config entry the + CLI error path cannot see.""" + home = tmp_path / "home" + cwd = tmp_path / "cwd" + home.mkdir() + cwd.mkdir() + + with pytest.raises(SystemExit): + _run(cwd, ["install", "--platform", "not-a-platform"], home) + + assert "omp" in capsys.readouterr().err