feat: add Devin CLI support (graphify devin install/uninstall) (#1020)
* feat: add Devin CLI support - Add 'devin' to _PLATFORM_CONFIG with skill-devin.md - Override _platform_skill_destination for devin: user scope -> ~/.config/devin/skills/graphify/SKILL.md project scope -> .devin/skills/graphify/SKILL.md - Add _DEVIN_RULES + _devin_rules_install/_uninstall: writes .windsurf/rules/graphify.md (always-on context) Devin reads .windsurf/rules/*.md the same way Windsurf IDE does - Add graphify devin install [--project] / uninstall subcommand - Create graphify/skill-devin.md with Devin YAML frontmatter (name, argument-hint, model, allowed-tools, triggers) - Add 21 tests in tests/test_devin.py covering user/project scope - Register _DEVIN_RULES in test_install_strings.py policy checks - Update README.md and pyproject.toml to list Devin CLI Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * expand skill-devin.md to full feature parity with skill-pi.md Rebuilds skill-devin.md from ~267 lines to 1176 lines, matching the depth of other mature platform skills (pi, droid, codex). Added: - Parallel Part A + B dispatch guidance (AST alongside semantic subagents) - Full subagent extraction prompt with rationale, semantic similarity, hyperedge, and confidence_score rules - Extraction cache check (Step B0) and cache save/load flow - --mode deep, --svg, --graphml, --neo4j/--neo4j-push, --mcp, --watch - /graphify path, /graphify explain, /graphify add, /graphify query - BFS vs DFS traversal with --dfs and --budget flags - Token reduction benchmark step - --update (incremental) and --cluster-only pipelines with graph diff - Video/audio transcription (Step 2.5) with Whisper domain-hint strategy - git commit hook section - Always-on Devin rules section referencing `graphify devin install` Updated test_devin.py: corrected python_c_syntax assertion to match the interpreter-detection pattern used across all mature skills. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: vishal-gandhi <ivishalgandhi@users.noreply.github.com> Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin
vishal-gandhi
parent
336652779b
commit
065a621fa6
@@ -137,6 +137,7 @@ for example `graphify claude install --project` or `graphify codex install --pro
|
||||
| Kiro IDE/CLI | `graphify kiro install` |
|
||||
| Pi coding agent | `graphify install --platform pi` |
|
||||
| Cursor | `graphify cursor install` |
|
||||
| Devin CLI | `graphify devin install` |
|
||||
| Google Antigravity | `graphify antigravity install` |
|
||||
|
||||
> Codex users: also add `multi_agent = true` under `[features]` in `~/.codex/config.toml`.
|
||||
@@ -187,6 +188,7 @@ Run this once in your project after building a graph:
|
||||
| Kimi Code | `graphify install --platform kimi` |
|
||||
| Kiro IDE/CLI | `graphify kiro install` |
|
||||
| Pi coding agent | `graphify pi install` |
|
||||
| Devin CLI | `graphify devin install` |
|
||||
| Google Antigravity | `graphify antigravity install` |
|
||||
|
||||
This writes a small config file that tells your assistant to consult the knowledge graph for codebase questions — preferring scoped queries like `graphify query "<question>"` over reading the full report or grepping raw files. On platforms that support payload-bearing hooks (Claude Code, Gemini CLI), a hook fires automatically before search-style tool calls and nudges your assistant toward the graph path. On the others (Codex, OpenCode, Cursor, etc.), the persistent instruction files (`AGENTS.md`, `.cursor/rules/`, etc.) provide the same query-first guidance. `GRAPH_REPORT.md` is still available for broad architecture review.
|
||||
@@ -466,6 +468,7 @@ graphify trae install / uninstall
|
||||
graphify trae-cn install / uninstall
|
||||
graphify hermes install / uninstall
|
||||
graphify kiro install / uninstall
|
||||
graphify devin install / uninstall
|
||||
graphify antigravity install / uninstall
|
||||
|
||||
graphify extract ./docs # headless LLM extraction for CI (no IDE needed)
|
||||
|
||||
+78
-1
@@ -77,6 +77,11 @@ def _platform_skill_destination(platform_name: str, *, project: bool = False, pr
|
||||
return Path.home() / ".agents" / "skills" / "graphify" / "SKILL.md"
|
||||
return Path.home() / ".gemini" / "skills" / "graphify" / "SKILL.md"
|
||||
|
||||
if platform_name == "devin":
|
||||
if project:
|
||||
return (project_dir or Path(".")) / ".devin" / "skills" / "graphify" / "SKILL.md"
|
||||
return Path.home() / ".config" / "devin" / "skills" / "graphify" / "SKILL.md"
|
||||
|
||||
cfg = _PLATFORM_CONFIG[platform_name]
|
||||
if project:
|
||||
return (project_dir or Path(".")) / cfg["skill_dst"]
|
||||
@@ -284,6 +289,13 @@ _PLATFORM_CONFIG: dict[str, dict] = {
|
||||
"skill_dst": Path(".kimi") / "skills" / "graphify" / "SKILL.md",
|
||||
"claude_md": False,
|
||||
},
|
||||
"devin": {
|
||||
"skill_file": "skill-devin.md",
|
||||
# User scope: ~/.config/devin/skills/graphify/SKILL.md
|
||||
# Project scope: .devin/skills/graphify/SKILL.md (overridden in _platform_skill_destination)
|
||||
"skill_dst": Path(".config") / "devin" / "skills" / "graphify" / "SKILL.md",
|
||||
"claude_md": False,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -854,6 +866,44 @@ def _cursor_uninstall(project_dir: Path) -> None:
|
||||
print(f"graphify Cursor rule removed from {rule_path.resolve()}")
|
||||
|
||||
|
||||
# Devin CLI — .windsurf/rules/graphify.md (always-on context)
|
||||
# Devin reads .windsurf/rules/*.md files the same way Windsurf IDE does.
|
||||
_DEVIN_RULES_PATH = Path(".windsurf") / "rules" / "graphify.md"
|
||||
_DEVIN_RULES_MARKER = "## graphify"
|
||||
_DEVIN_RULES = """\
|
||||
## graphify
|
||||
|
||||
This project has a graphify knowledge graph at graphify-out/.
|
||||
|
||||
Rules:
|
||||
- For codebase or architecture questions, when `graphify-out/graph.json` exists, first run `graphify query "<question>"` (or `graphify path "<A>" "<B>"` / `graphify explain "<concept>"`). These return a scoped subgraph, usually much smaller than `GRAPH_REPORT.md` or raw grep output.
|
||||
- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files
|
||||
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context
|
||||
- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost)
|
||||
"""
|
||||
|
||||
|
||||
def _devin_rules_install(project_dir: Path) -> None:
|
||||
"""Write .windsurf/rules/graphify.md for always-on Devin context."""
|
||||
rules_path = (project_dir or Path(".")) / _DEVIN_RULES_PATH
|
||||
rules_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
if rules_path.exists() and rules_path.read_text(encoding="utf-8") == _DEVIN_RULES:
|
||||
print(f" {rules_path} -> already configured (no change)")
|
||||
return
|
||||
action = "updated" if rules_path.exists() else "written"
|
||||
rules_path.write_text(_DEVIN_RULES, encoding="utf-8")
|
||||
print(f" rules {action} -> {rules_path}")
|
||||
|
||||
|
||||
def _devin_rules_uninstall(project_dir: Path) -> None:
|
||||
"""Remove .windsurf/rules/graphify.md."""
|
||||
rules_path = (project_dir or Path(".")) / _DEVIN_RULES_PATH
|
||||
if not rules_path.exists():
|
||||
return
|
||||
rules_path.unlink()
|
||||
print(f" rules removed -> {rules_path}")
|
||||
|
||||
|
||||
# OpenCode tool.execute.before plugin — fires before every tool call.
|
||||
# Injects a graph reminder into bash command output when graph.json exists.
|
||||
_OPENCODE_PLUGIN_JS = """\
|
||||
@@ -1080,6 +1130,10 @@ def _project_install(platform_name: str, project_dir: Path | None = None) -> Non
|
||||
elif platform_name == "codex":
|
||||
hint_paths.append(project_dir / ".codex")
|
||||
_print_project_git_add_hint(hint_paths)
|
||||
elif platform_name == "devin":
|
||||
skill_dst = _copy_skill_file("devin", project=True, project_dir=project_dir)
|
||||
_devin_rules_install(project_dir)
|
||||
_print_project_git_add_hint([_project_scope_root(skill_dst, project_dir), project_dir / ".windsurf"])
|
||||
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)])
|
||||
@@ -1107,6 +1161,11 @@ def _project_uninstall(platform_name: str, project_dir: Path | None = None) -> N
|
||||
_uninstall_codex_hook(project_dir)
|
||||
elif platform_name == "antigravity":
|
||||
_antigravity_uninstall(project_dir)
|
||||
elif platform_name == "devin":
|
||||
removed = _remove_skill_file("devin", project=True, project_dir=project_dir)
|
||||
_devin_rules_uninstall(project_dir)
|
||||
if not removed:
|
||||
print("nothing to remove")
|
||||
elif platform_name in ("copilot", "pi", "kimi"):
|
||||
removed = _remove_skill_file(platform_name, project=True, project_dir=project_dir)
|
||||
if not removed:
|
||||
@@ -1366,7 +1425,7 @@ def main() -> None:
|
||||
print("Usage: graphify <command>")
|
||||
print()
|
||||
print("Commands:")
|
||||
print(" install [--platform P] copy skill to platform config dir (claude|windows|codex|opencode|aider|claw|droid|trae|trae-cn|gemini|cursor|antigravity|hermes|kiro|pi)")
|
||||
print(" install [--platform P] copy skill to platform config dir (claude|windows|codex|opencode|aider|claw|droid|trae|trae-cn|gemini|cursor|antigravity|hermes|kiro|pi|devin)")
|
||||
print(" uninstall remove graphify from all detected platforms in one shot")
|
||||
print(" --purge also delete graphify-out/ directory")
|
||||
print(" path \"A\" \"B\" shortest path between two nodes in graph.json")
|
||||
@@ -1478,6 +1537,8 @@ def main() -> None:
|
||||
print(" kiro uninstall remove skill + steering file")
|
||||
print(" pi install write skill to ~/.pi/agent/skills/graphify/ (Pi coding agent)")
|
||||
print(" pi uninstall remove skill from ~/.pi/agent/skills/graphify/")
|
||||
print(" devin install write skill to ~/.config/devin/skills/graphify/ (Devin CLI)")
|
||||
print(" devin uninstall remove skill from ~/.config/devin/skills/graphify/")
|
||||
print()
|
||||
return
|
||||
|
||||
@@ -1638,6 +1699,22 @@ def main() -> None:
|
||||
else:
|
||||
print("Usage: graphify kiro [install|uninstall]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif cmd == "devin":
|
||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if subcmd == "install":
|
||||
if "--project" in sys.argv[3:]:
|
||||
_project_install("devin", Path("."))
|
||||
else:
|
||||
install(platform="devin")
|
||||
elif subcmd == "uninstall":
|
||||
if "--project" in sys.argv[3:]:
|
||||
_project_uninstall("devin", Path("."))
|
||||
else:
|
||||
removed = _remove_skill_file("devin")
|
||||
print("skill removed" if removed else "nothing to remove")
|
||||
else:
|
||||
print("Usage: graphify devin [install|uninstall]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
elif cmd == "pi":
|
||||
subcmd = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||
if subcmd == "install":
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+3
-3
@@ -5,10 +5,10 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "graphifyy"
|
||||
version = "0.8.18"
|
||||
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
|
||||
description = "AI coding assistant skill (Claude Code, Codex, OpenCode, Cursor, Gemini CLI, Aider, OpenClaw, Factory Droid, Trae, Hermes, Kiro, Pi, Devin CLI, Google Antigravity) - turn any folder of code, docs, papers, images, or videos into a queryable knowledge graph"
|
||||
readme = "README.md"
|
||||
license = { file = "LICENSE" }
|
||||
keywords = ["claude", "claude-code", "codex", "opencode", "cursor", "gemini", "aider", "kiro", "pi", "knowledge-graph", "rag", "graphrag", "obsidian", "community-detection", "tree-sitter", "leiden", "llm"]
|
||||
keywords = ["claude", "claude-code", "codex", "opencode", "cursor", "gemini", "aider", "kiro", "pi", "devin", "knowledge-graph", "rag", "graphrag", "obsidian", "community-detection", "tree-sitter", "leiden", "llm"]
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"networkx",
|
||||
@@ -96,7 +96,7 @@ packages = ["graphify"]
|
||||
include-package-data = false
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-aider.md", "skill-copilot.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md", "skill-kiro.md", "skill-vscode.md", "skill-pi.md"]
|
||||
graphify = ["skill.md", "skill-codex.md", "skill-opencode.md", "skill-aider.md", "skill-copilot.md", "skill-claw.md", "skill-windows.md", "skill-droid.md", "skill-trae.md", "skill-kiro.md", "skill-vscode.md", "skill-pi.md", "skill-devin.md"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
"""Tests for graphify devin install / uninstall commands."""
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
import pytest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _devin_install_user(tmp_path):
|
||||
from graphify.__main__ import install
|
||||
old_cwd = Path.cwd()
|
||||
try:
|
||||
import os
|
||||
os.chdir(tmp_path)
|
||||
with patch("graphify.__main__.Path.home", return_value=tmp_path):
|
||||
install(platform="devin")
|
||||
finally:
|
||||
import os
|
||||
os.chdir(old_cwd)
|
||||
|
||||
|
||||
def _skill_path_user(tmp_path):
|
||||
return tmp_path / ".config" / "devin" / "skills" / "graphify" / "SKILL.md"
|
||||
|
||||
|
||||
def _skill_path_project(project_dir):
|
||||
return project_dir / ".devin" / "skills" / "graphify" / "SKILL.md"
|
||||
|
||||
|
||||
def _rules_path(project_dir):
|
||||
return project_dir / ".windsurf" / "rules" / "graphify.md"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# User-scope install (graphify install --platform devin / graphify devin install)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_devin_install_user_creates_skill_file(tmp_path):
|
||||
"""User-scope install copies skill to ~/.config/devin/skills/graphify/SKILL.md."""
|
||||
_devin_install_user(tmp_path)
|
||||
skill_path = _skill_path_user(tmp_path)
|
||||
assert skill_path.exists()
|
||||
|
||||
|
||||
def test_devin_skill_file_contains_frontmatter(tmp_path):
|
||||
"""Installed skill file must include Devin-specific YAML frontmatter."""
|
||||
_devin_install_user(tmp_path)
|
||||
content = _skill_path_user(tmp_path).read_text()
|
||||
assert "name: graphify" in content
|
||||
assert "argument-hint:" in content
|
||||
assert "triggers:" in content
|
||||
|
||||
|
||||
def test_devin_skill_file_references_graphify_query(tmp_path):
|
||||
"""/graphify skill must mention graphify query (query-first policy)."""
|
||||
_devin_install_user(tmp_path)
|
||||
content = _skill_path_user(tmp_path).read_text()
|
||||
assert "graphify query" in content or "/graphify query" in content
|
||||
|
||||
|
||||
def test_devin_install_user_does_not_write_rules(tmp_path):
|
||||
"""User-scope install does NOT write .windsurf/rules/ — that's project-only."""
|
||||
_devin_install_user(tmp_path)
|
||||
assert not _rules_path(tmp_path).exists()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Project-scope install (graphify devin install --project)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_devin_install_project_creates_skill_file(tmp_path, monkeypatch):
|
||||
"""Project-scope install copies skill to .devin/skills/graphify/SKILL.md."""
|
||||
from graphify.__main__ import main
|
||||
home = tmp_path / "home"
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
monkeypatch.chdir(project)
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "devin", "install", "--project"])
|
||||
with patch("graphify.__main__.Path.home", return_value=home):
|
||||
main()
|
||||
assert _skill_path_project(project).exists()
|
||||
assert not _skill_path_user(home).exists()
|
||||
|
||||
|
||||
def test_devin_install_project_creates_rules_file(tmp_path, monkeypatch):
|
||||
"""Project-scope install writes .windsurf/rules/graphify.md."""
|
||||
from graphify.__main__ import main
|
||||
home = tmp_path / "home"
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
monkeypatch.chdir(project)
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "devin", "install", "--project"])
|
||||
with patch("graphify.__main__.Path.home", return_value=home):
|
||||
main()
|
||||
rules = _rules_path(project)
|
||||
assert rules.exists()
|
||||
assert "graphify" in rules.read_text()
|
||||
assert "GRAPH_REPORT.md" in rules.read_text()
|
||||
|
||||
|
||||
def test_devin_rules_content_recommends_graphify_query(tmp_path):
|
||||
"""The rules file installed by devin must use query-first policy."""
|
||||
from graphify.__main__ import _devin_rules_install
|
||||
_devin_rules_install(tmp_path)
|
||||
content = _rules_path(tmp_path).read_text()
|
||||
assert "graphify query" in content
|
||||
|
||||
|
||||
def test_devin_rules_install_idempotent(tmp_path, capsys):
|
||||
"""Installing rules twice does not change content and prints 'no change'."""
|
||||
from graphify.__main__ import _devin_rules_install
|
||||
_devin_rules_install(tmp_path)
|
||||
content_first = _rules_path(tmp_path).read_text()
|
||||
_devin_rules_install(tmp_path)
|
||||
content_second = _rules_path(tmp_path).read_text()
|
||||
assert content_first == content_second
|
||||
assert "no change" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_devin_install_project_hints_git_add(tmp_path, monkeypatch, capsys):
|
||||
"""Project-scope install prints a git add hint covering .devin/ and .windsurf/."""
|
||||
from graphify.__main__ import main
|
||||
home = tmp_path / "home"
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
monkeypatch.chdir(project)
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "devin", "install", "--project"])
|
||||
with patch("graphify.__main__.Path.home", return_value=home):
|
||||
main()
|
||||
out = capsys.readouterr().out
|
||||
assert "git add" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Uninstall — user scope
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_devin_uninstall_user_removes_skill_file(tmp_path):
|
||||
"""User-scope uninstall removes the skill file."""
|
||||
_devin_install_user(tmp_path)
|
||||
skill = _skill_path_user(tmp_path)
|
||||
assert skill.exists()
|
||||
|
||||
from graphify.__main__ import _remove_skill_file
|
||||
with patch("graphify.__main__.Path.home", return_value=tmp_path):
|
||||
_remove_skill_file("devin")
|
||||
assert not skill.exists()
|
||||
|
||||
|
||||
def test_devin_uninstall_user_noop_when_not_installed(tmp_path, capsys):
|
||||
"""User-scope uninstall prints an appropriate message when nothing is installed."""
|
||||
from graphify.__main__ import main
|
||||
import os
|
||||
old_cwd = Path.cwd()
|
||||
try:
|
||||
os.chdir(tmp_path)
|
||||
with patch("graphify.__main__.Path.home", return_value=tmp_path):
|
||||
sys.argv = ["graphify", "devin", "uninstall"]
|
||||
main()
|
||||
finally:
|
||||
os.chdir(old_cwd)
|
||||
out = capsys.readouterr().out
|
||||
assert "nothing to remove" in out
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Uninstall — project scope
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_devin_uninstall_project_removes_skill_file(tmp_path, monkeypatch):
|
||||
"""Project-scope uninstall removes .devin/skills/graphify/SKILL.md."""
|
||||
from graphify.__main__ import main
|
||||
home = tmp_path / "home"
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
monkeypatch.chdir(project)
|
||||
with patch("graphify.__main__.Path.home", return_value=home):
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "devin", "install", "--project"])
|
||||
main()
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "devin", "uninstall", "--project"])
|
||||
main()
|
||||
assert not _skill_path_project(project).exists()
|
||||
|
||||
|
||||
def test_devin_uninstall_project_removes_rules_file(tmp_path, monkeypatch):
|
||||
"""Project-scope uninstall removes .windsurf/rules/graphify.md."""
|
||||
from graphify.__main__ import main
|
||||
home = tmp_path / "home"
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
monkeypatch.chdir(project)
|
||||
with patch("graphify.__main__.Path.home", return_value=home):
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "devin", "install", "--project"])
|
||||
main()
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "devin", "uninstall", "--project"])
|
||||
main()
|
||||
assert not _rules_path(project).exists()
|
||||
|
||||
|
||||
def test_devin_uninstall_project_does_not_touch_user_scope(tmp_path, monkeypatch):
|
||||
"""Project-scope uninstall must not remove the user-scope skill file."""
|
||||
from graphify.__main__ import main
|
||||
home = tmp_path / "home"
|
||||
project = tmp_path / "project"
|
||||
project.mkdir()
|
||||
# Pre-create a user-scope skill file
|
||||
user_skill = _skill_path_user(home)
|
||||
user_skill.parent.mkdir(parents=True, exist_ok=True)
|
||||
user_skill.write_text("user skill")
|
||||
monkeypatch.chdir(project)
|
||||
with patch("graphify.__main__.Path.home", return_value=home):
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "devin", "install", "--project"])
|
||||
main()
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "devin", "uninstall", "--project"])
|
||||
main()
|
||||
assert user_skill.exists()
|
||||
|
||||
|
||||
def test_devin_rules_uninstall_noop_when_not_installed(tmp_path):
|
||||
"""_devin_rules_uninstall does nothing if the rules file was never written."""
|
||||
from graphify.__main__ import _devin_rules_uninstall
|
||||
_devin_rules_uninstall(tmp_path) # should not raise
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Skill file content
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_devin_skill_file_exists_in_package():
|
||||
"""skill-devin.md must be present in the installed package."""
|
||||
import graphify
|
||||
skill = Path(graphify.__file__).parent / "skill-devin.md"
|
||||
assert skill.exists(), "skill-devin.md missing from package"
|
||||
|
||||
|
||||
def test_devin_skill_file_uses_python_c_syntax():
|
||||
"""Devin skill must use inline python -c syntax (cross-platform, no bash heredocs).
|
||||
|
||||
All mature graphify skills use the interpreter-detection pattern
|
||||
``$(cat graphify-out/.graphify_python) -c "..."`` rather than bare
|
||||
``python -c "..."`` so they work in pipx / venv environments.
|
||||
"""
|
||||
import graphify
|
||||
skill = (Path(graphify.__file__).parent / "skill-devin.md").read_text()
|
||||
assert '.graphify_python) -c "' in skill, (
|
||||
"skill-devin.md must use the interpreter-detection pattern "
|
||||
"'$(cat graphify-out/.graphify_python) -c \"...\"'"
|
||||
)
|
||||
assert "#!/bin/bash" not in skill
|
||||
|
||||
|
||||
def test_devin_skill_file_frontmatter_has_triggers():
|
||||
"""Devin skill frontmatter must list triggers for model-invocable activation."""
|
||||
import graphify
|
||||
skill = (Path(graphify.__file__).parent / "skill-devin.md").read_text()
|
||||
assert "triggers:" in skill
|
||||
assert "model" in skill
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Platform config sanity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_devin_in_platform_config():
|
||||
"""devin must be registered in _PLATFORM_CONFIG."""
|
||||
from graphify.__main__ import _PLATFORM_CONFIG
|
||||
assert "devin" in _PLATFORM_CONFIG
|
||||
assert _PLATFORM_CONFIG["devin"]["skill_file"] == "skill-devin.md"
|
||||
assert _PLATFORM_CONFIG["devin"]["claude_md"] is False
|
||||
|
||||
|
||||
def test_devin_platform_skill_destination_user_scope(tmp_path):
|
||||
"""User-scope destination must be ~/.config/devin/skills/graphify/SKILL.md."""
|
||||
from graphify.__main__ import _platform_skill_destination
|
||||
with patch("graphify.__main__.Path.home", return_value=tmp_path):
|
||||
dst = _platform_skill_destination("devin", project=False)
|
||||
assert dst == tmp_path / ".config" / "devin" / "skills" / "graphify" / "SKILL.md"
|
||||
|
||||
|
||||
def test_devin_in_main_help_text(capsys, monkeypatch):
|
||||
"""`graphify --help` must list devin in the platform list and in the per-platform section."""
|
||||
from graphify.__main__ import main
|
||||
monkeypatch.setattr(sys, "argv", ["graphify", "--help"])
|
||||
main()
|
||||
captured = capsys.readouterr().out
|
||||
# devin should appear in the top-level platform list
|
||||
assert "|devin)" in captured or "|devin |" in captured or "|devin" in captured, (
|
||||
"devin missing from `graphify --help` platform list"
|
||||
)
|
||||
# devin install / uninstall should appear in the per-platform section
|
||||
assert "devin install" in captured, "`devin install` line missing from help text"
|
||||
assert "devin uninstall" in captured, "`devin uninstall` line missing from help text"
|
||||
assert "~/.config/devin" in captured, "devin user-scope path missing from help text"
|
||||
# Convention: `--project` is supported by all platforms but documented by none.
|
||||
# devin should not be the lone outlier that documents it.
|
||||
devin_section = captured.split("devin install", 1)[1].split("\n\n", 1)[0]
|
||||
assert "--project" not in devin_section, (
|
||||
"devin help should NOT document --project — no other platform does"
|
||||
)
|
||||
|
||||
|
||||
def test_devin_platform_skill_destination_project_scope(tmp_path):
|
||||
"""Project-scope destination must be <project>/.devin/skills/graphify/SKILL.md."""
|
||||
from graphify.__main__ import _platform_skill_destination
|
||||
dst = _platform_skill_destination("devin", project=True, project_dir=tmp_path)
|
||||
assert dst == tmp_path / ".devin" / "skills" / "graphify" / "SKILL.md"
|
||||
@@ -22,6 +22,7 @@ from graphify.__main__ import (
|
||||
_KIRO_STEERING,
|
||||
_CURSOR_RULE,
|
||||
_OPENCODE_PLUGIN_JS,
|
||||
_DEVIN_RULES,
|
||||
)
|
||||
|
||||
|
||||
@@ -39,6 +40,7 @@ _INSTALL_TEXTS: dict[str, str] = {
|
||||
"_KIRO_STEERING": _KIRO_STEERING,
|
||||
"_CURSOR_RULE": _CURSOR_RULE,
|
||||
"_OPENCODE_PLUGIN_JS": _OPENCODE_PLUGIN_JS,
|
||||
"_DEVIN_RULES": _DEVIN_RULES,
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +107,7 @@ def test_report_is_still_referenced_as_fallback():
|
||||
"_ANTIGRAVITY_RULES": _ANTIGRAVITY_RULES,
|
||||
"_KIRO_STEERING": _KIRO_STEERING,
|
||||
"_CURSOR_RULE": _CURSOR_RULE,
|
||||
"_DEVIN_RULES": _DEVIN_RULES,
|
||||
}
|
||||
missing: list[str] = []
|
||||
for name, text in md_section_texts.items():
|
||||
|
||||
Reference in New Issue
Block a user