Skip to content

Commit 7b4b01c

Browse files
committed
feat: deploy cmi agent skills via diffpy.app agentify
1 parent 85d1242 commit 7b4b01c

4 files changed

Lines changed: 193 additions & 0 deletions

File tree

news/agentify.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* Add "agentify" app to deploy ``diffpy.cmi`` agent skills.
4+
5+
**Changed:**
6+
7+
* <news item>
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

src/diffpy/apps/app_agentify.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import shutil
2+
import subprocess
3+
import tempfile
4+
from pathlib import Path
5+
6+
REPO_URL = "https://github.com/diffpy/cmi-agent-skills"
7+
DIR_NAME = "cmi-skill"
8+
9+
10+
def agentify(args):
11+
agent = args.agent
12+
system_flag = args.system
13+
if agent == "claude":
14+
skills_dir = ".claude/skills"
15+
elif agent == "codex":
16+
skills_dir = ".codex/skills"
17+
if system_flag:
18+
destination = Path().home() / skills_dir / DIR_NAME
19+
else:
20+
destination = Path().cwd() / skills_dir / DIR_NAME
21+
if destination.exists() and not args.update:
22+
raise FileExistsError(
23+
f"Agentic skill {DIR_NAME} already exists at {destination}. "
24+
"To overwrite, pass '--update' flag to update the skill"
25+
)
26+
with tempfile.TemporaryDirectory() as tmp:
27+
tmp_path = Path(tmp)
28+
subprocess.run(
29+
["git", "clone", REPO_URL, str(tmp_path)],
30+
check=True,
31+
)
32+
if destination.exists():
33+
shutil.rmtree(destination)
34+
shutil.copytree(tmp_path / DIR_NAME, destination, dirs_exist_ok=True)
35+
print(f"Agentic skill {DIR_NAME} has been deployed to {destination}")

src/diffpy/apps/apps.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import argparse
22

3+
from diffpy.apps.app_agentify import agentify
34
from diffpy.apps.app_runmacro import runmacro
45
from diffpy.apps.version import __version__ # noqa
56

@@ -36,6 +37,7 @@ def main():
3637
title="Available applications",
3738
dest="application",
3839
)
40+
# runmacro application
3941
runmacro_parser = apps_parsers.add_parser(
4042
"runmacro",
4143
help="Run a macro `<.dp-in>` file",
@@ -46,6 +48,29 @@ def main():
4648
help="Path to the `<.dp-in>` macro file to be run",
4749
)
4850
runmacro_parser.set_defaults(func=runmacro)
51+
# agent application
52+
agentify_parser = apps_parsers.add_parser(
53+
"agentify",
54+
help="Deploy diffpy.cmi agentic skills in the local environment.",
55+
)
56+
agentify_parser.add_argument(
57+
"--agent",
58+
"-a",
59+
help="The agent to use for the agentic skill.",
60+
default="claude",
61+
choices=["claude", "codex"],
62+
)
63+
agentify_parser.add_argument(
64+
"--update",
65+
action="store_true",
66+
help="When set, update the existing agentic skill.",
67+
)
68+
agentify_parser.add_argument(
69+
"--system",
70+
action="store_true",
71+
help="When set, deploy the agentic skill to the system directory.",
72+
)
73+
agentify_parser.set_defaults(func=agentify)
4974
args = parser.parse_args()
5075
if args.application is None:
5176
parser.print_help()

tests/test_agentify.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import re
2+
import tempfile
3+
from pathlib import Path
4+
from types import SimpleNamespace
5+
from unittest import mock
6+
7+
import pytest
8+
9+
from diffpy.apps.app_agentify import agentify
10+
11+
12+
@pytest.mark.parametrize(
13+
"args, expected_scope, expected_skill_dir",
14+
[
15+
# C1: diffpy.apps agentify
16+
# Deploys workspace claude skill.
17+
# Expect skill folder is created in the current working directory.
18+
(
19+
SimpleNamespace(
20+
agent="claude",
21+
system=False,
22+
update=False,
23+
),
24+
"cwd",
25+
".claude/skills/cmi-skill",
26+
),
27+
# C2: diffpy.apps agentify --system
28+
# Deploys system claude skill.
29+
# Expect skill folder is created in the user's home directory.
30+
(
31+
SimpleNamespace(
32+
agent="claude",
33+
system=True,
34+
update=False,
35+
),
36+
"home",
37+
".claude/skills/cmi-skill",
38+
),
39+
# C3: diffpy.apps agentify --agent codex
40+
# Deploys workspace codex skill.
41+
# Expect skill folder is created in the current working directory.
42+
(
43+
SimpleNamespace(
44+
agent="codex",
45+
system=False,
46+
update=False,
47+
),
48+
"cwd",
49+
".codex/skills/cmi-skill",
50+
),
51+
# C4: diffpy.apps agentify --agent codex --system
52+
# Deploys system codex skill.
53+
# Expect skill folder is created in the user's home directory.
54+
(
55+
SimpleNamespace(
56+
agent="codex",
57+
system=True,
58+
update=False,
59+
),
60+
"home",
61+
".codex/skills/cmi-skill",
62+
),
63+
],
64+
)
65+
def test_agentify(args, expected_scope, expected_skill_dir):
66+
with tempfile.TemporaryDirectory() as tmp:
67+
with (
68+
mock.patch.object(Path, "home", return_value=Path(tmp) / "home"),
69+
mock.patch.object(Path, "cwd", return_value=Path(tmp) / "cwd"),
70+
):
71+
agentify(args)
72+
expected_path = Path(tmp) / expected_scope / expected_skill_dir
73+
assert expected_path.exists()
74+
75+
76+
def test_agentify_update():
77+
with tempfile.TemporaryDirectory() as tmp:
78+
with (
79+
mock.patch.object(Path, "home", return_value=Path(tmp) / "home"),
80+
mock.patch.object(Path, "cwd", return_value=Path(tmp) / "cwd"),
81+
):
82+
# C1: Deploy again without --update flag when skill already exists.
83+
# Expect FileExistsError to be raised, and the error message
84+
# matches.
85+
args = SimpleNamespace(
86+
agent="claude",
87+
system=False,
88+
update=False,
89+
)
90+
agentify(args)
91+
skill_path = Path(tmp) / "cwd" / ".claude" / "skills" / "cmi-skill"
92+
assert skill_path.exists()
93+
args.update = True
94+
agentify(args)
95+
pytest.raises(
96+
FileExistsError,
97+
match=re.escape(
98+
f"Agentic skill cmi-skill already exists at {skill_path}. "
99+
"To overwrite, pass '--update' flag to update the skill"
100+
),
101+
)
102+
# C1: Deploy again with --update flag when skill already exists
103+
# with a dummy file in the skill directory.
104+
# Expect no error to be raised, and the skill is updated.
105+
dummy_file = skill_path / "dummy.txt"
106+
dummy_file.touch()
107+
assert dummy_file.exists()
108+
args.update = True
109+
agentify(args)
110+
assert not dummy_file.exists()

0 commit comments

Comments
 (0)