mirror of
https://github.com/lllin000/PaperForge.git
synced 2026-07-22 06:50:53 +00:00
refactor(skills): simplify skill deployment — single copytree for all platforms
- Remove format dispatch (flat_command/skill_directory/rules_file) — all platforms unified - skill_deploy.py: 257→81 lines, only AGENT_SKILL_DIRS + copytree + AGENTS.md - AgentInstaller switches to vault-local paths, removes deploy_commands step - Remove _deploy_skills dead code in update.py
This commit is contained in:
parent
d50c2ff4c0
commit
c0cc05ab32
3 changed files with 57 additions and 311 deletions
|
|
@ -1,4 +1,4 @@
|
|||
"""Skill deployment service — single source of truth for agent skill installation and updates.
|
||||
"""Skill deployment service — single copytree for all platforms.
|
||||
|
||||
Used by both setup wizard (install) and update worker (update).
|
||||
All deployments are vault-local only.
|
||||
|
|
@ -9,253 +9,82 @@ from __future__ import annotations
|
|||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
# ── Agent Platform Configurations ──
|
||||
# Canonical source of truth. setup_wizard.py imports from here.
|
||||
|
||||
AGENT_CONFIGS = {
|
||||
"opencode": {
|
||||
"name": "OpenCode",
|
||||
"skill_dir": ".opencode/skills",
|
||||
"command_dir": ".opencode/command",
|
||||
"format": "flat_command",
|
||||
"prefix": "/",
|
||||
"config_file": None,
|
||||
},
|
||||
"claude": {
|
||||
"name": "Claude Code",
|
||||
"skill_dir": ".claude/skills",
|
||||
"format": "skill_directory",
|
||||
"prefix": "/",
|
||||
"config_file": ".claude/skills.json",
|
||||
},
|
||||
"codex": {
|
||||
"name": "Codex",
|
||||
"skill_dir": ".codex/skills",
|
||||
"format": "skill_directory",
|
||||
"prefix": "$",
|
||||
"config_file": None,
|
||||
},
|
||||
"cursor": {
|
||||
"name": "Cursor",
|
||||
"skill_dir": ".cursor/skills",
|
||||
"format": "skill_directory",
|
||||
"prefix": "/",
|
||||
"config_file": ".cursor/settings.json",
|
||||
},
|
||||
"windsurf": {
|
||||
"name": "Windsurf",
|
||||
"skill_dir": ".windsurf/skills",
|
||||
"format": "skill_directory",
|
||||
"prefix": "/",
|
||||
"config_file": None,
|
||||
},
|
||||
"github_copilot": {
|
||||
"name": "GitHub Copilot",
|
||||
"skill_dir": ".github/skills",
|
||||
"format": "skill_directory",
|
||||
"prefix": "/",
|
||||
"config_file": ".github/copilot-instructions.md",
|
||||
},
|
||||
"cline": {
|
||||
"name": "Cline",
|
||||
"skill_dir": ".clinerules",
|
||||
"format": "rules_file",
|
||||
"prefix": "/",
|
||||
"config_file": ".clinerules",
|
||||
},
|
||||
"augment": {
|
||||
"name": "Augment",
|
||||
"skill_dir": ".augment/skills",
|
||||
"format": "skill_directory",
|
||||
"prefix": "/",
|
||||
"config_file": None,
|
||||
},
|
||||
"trae": {
|
||||
"name": "Trae",
|
||||
"skill_dir": ".trae/skills",
|
||||
"format": "skill_directory",
|
||||
"prefix": "/",
|
||||
"config_file": None,
|
||||
},
|
||||
# ── Agent platform → vault-local skill directory ──
|
||||
AGENT_SKILL_DIRS: dict[str, str] = {
|
||||
"opencode": ".opencode/skills",
|
||||
"claude": ".claude/skills",
|
||||
"codex": ".codex/skills",
|
||||
"cursor": ".cursor/skills",
|
||||
"windsurf": ".windsurf/skills",
|
||||
"github_copilot": ".github/skills",
|
||||
"cline": ".clinerules",
|
||||
"augment": ".augment/skills",
|
||||
"trae": ".trae/skills",
|
||||
}
|
||||
|
||||
|
||||
def _resolve_source_root() -> Path:
|
||||
"""Resolve the paperforge package root (where skills/ lives)."""
|
||||
import paperforge
|
||||
|
||||
return Path(paperforge.__file__).parent
|
||||
|
||||
|
||||
def _substitute_vars(text: str, system_dir: str, resources_dir: str, literature_dir: str, base_dir: str, skill_dir: str, prefix: str = "/") -> str:
|
||||
for old, new in [
|
||||
("<system_dir>", system_dir),
|
||||
("<resources_dir>", resources_dir),
|
||||
("<literature_dir>", literature_dir),
|
||||
("<base_dir>", base_dir),
|
||||
("<skill_dir>", skill_dir),
|
||||
("<prefix>", prefix),
|
||||
]:
|
||||
text = text.replace(old, new)
|
||||
return text
|
||||
|
||||
|
||||
# ── Deploy helpers ──
|
||||
|
||||
def _deploy_skill_directory(vault: Path, skill_dir: str, source_root: Path, system_dir: str, resources_dir: str, literature_dir: str, base_dir: str, prefix: str = "/", overwrite: bool = False) -> list[str]:
|
||||
"""Deploy pf-* skills as independent SKILL.md directories (Claude Code, Codex, Cursor, etc.)."""
|
||||
imported = []
|
||||
src_scripts = source_root / "skills" / "literature-qa" / "scripts"
|
||||
src_charts = source_root / "skills" / "literature-qa" / "chart-reading"
|
||||
src_prompt = source_root / "skills" / "literature-qa" / "prompt_deep_subagent.md"
|
||||
|
||||
for skill_file in sorted(src_scripts.glob("pf-*.md")):
|
||||
skill_name = skill_file.stem
|
||||
skill_dst = vault / skill_dir / skill_name
|
||||
skill_dst.mkdir(parents=True, exist_ok=True)
|
||||
text = skill_file.read_text(encoding="utf-8")
|
||||
text = _substitute_vars(text, system_dir, resources_dir, literature_dir, base_dir, skill_dir, prefix)
|
||||
dst_file = skill_dst / "SKILL.md"
|
||||
if overwrite or not dst_file.exists():
|
||||
dst_file.write_text(text, encoding="utf-8")
|
||||
imported.append(skill_name)
|
||||
|
||||
# pf-deep extras: scripts, chart-reading, subagent prompt
|
||||
pf_deep_dst = vault / skill_dir / "pf-deep"
|
||||
pf_deep_dst.mkdir(parents=True, exist_ok=True)
|
||||
ld_dst = pf_deep_dst / "scripts" / "ld_deep.py"
|
||||
ld_src = src_scripts / "ld_deep.py"
|
||||
if ld_src.exists() and (overwrite or not ld_dst.exists()):
|
||||
ld_dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ld_src, ld_dst)
|
||||
prompt_dst = pf_deep_dst / "prompt_deep_subagent.md"
|
||||
if src_prompt.exists() and (overwrite or not prompt_dst.exists()):
|
||||
shutil.copy2(src_prompt, prompt_dst)
|
||||
if src_charts.exists() and src_charts.is_dir():
|
||||
chart_dst = pf_deep_dst / "chart-reading"
|
||||
if overwrite and chart_dst.exists():
|
||||
shutil.rmtree(chart_dst)
|
||||
chart_dst.mkdir(parents=True, exist_ok=True)
|
||||
for f in src_charts.glob("*.md"):
|
||||
if overwrite or not (chart_dst / f.name).exists():
|
||||
shutil.copy2(f, chart_dst / f.name)
|
||||
|
||||
return imported
|
||||
|
||||
|
||||
def _deploy_flat_command(vault: Path, command_dir: str, source_root: Path, system_dir: str, resources_dir: str, literature_dir: str, base_dir: str, skill_dir: str, overwrite: bool = False) -> list[str]:
|
||||
"""Deploy skills in flat .md command format (OpenCode)."""
|
||||
imported = []
|
||||
command_src = source_root / "skills" / "literature-qa" / "scripts"
|
||||
command_dst = vault / command_dir
|
||||
if not (command_src.exists() and command_src.is_dir()):
|
||||
return imported
|
||||
|
||||
command_dst.mkdir(parents=True, exist_ok=True)
|
||||
for f in command_src.glob("pf-*.md"):
|
||||
text = f.read_text(encoding="utf-8")
|
||||
text = _substitute_vars(text, system_dir, resources_dir, literature_dir, base_dir, skill_dir)
|
||||
dst_file = command_dst / f.name
|
||||
if overwrite or not dst_file.exists():
|
||||
dst_file.write_text(text, encoding="utf-8")
|
||||
imported.append(f.stem)
|
||||
|
||||
return imported
|
||||
|
||||
|
||||
def _deploy_rules_file(vault: Path, skill_dir: str, source_root: Path, system_dir: str, resources_dir: str, literature_dir: str, base_dir: str, overwrite: bool = False) -> list[str]:
|
||||
"""Deploy skills as .clinerules directory (Cline)."""
|
||||
imported = []
|
||||
src_scripts = source_root / "skills" / "literature-qa" / "scripts"
|
||||
src_charts = source_root / "skills" / "literature-qa" / "chart-reading"
|
||||
src_prompt = source_root / "skills" / "literature-qa" / "prompt_deep_subagent.md"
|
||||
|
||||
pf_deep_dst = vault / skill_dir / "pf-deep"
|
||||
pf_deep_dst.mkdir(parents=True, exist_ok=True)
|
||||
ld_src = src_scripts / "ld_deep.py"
|
||||
ld_dst = pf_deep_dst / "scripts" / "ld_deep.py"
|
||||
if ld_src.exists() and (overwrite or not ld_dst.exists()):
|
||||
ld_dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(ld_src, ld_dst)
|
||||
prompt_dst = pf_deep_dst / "prompt_deep_subagent.md"
|
||||
if src_prompt.exists() and (overwrite or not prompt_dst.exists()):
|
||||
shutil.copy2(src_prompt, prompt_dst)
|
||||
if src_charts.exists() and src_charts.is_dir():
|
||||
chart_dst = pf_deep_dst / "chart-reading"
|
||||
if overwrite and chart_dst.exists():
|
||||
shutil.rmtree(chart_dst)
|
||||
chart_dst.mkdir(parents=True, exist_ok=True)
|
||||
for f in src_charts.glob("*.md"):
|
||||
if overwrite or not (chart_dst / f.name).exists():
|
||||
shutil.copy2(f, chart_dst / f.name)
|
||||
|
||||
imported.append("clinerules")
|
||||
return imported
|
||||
|
||||
|
||||
# ── Main entry point ──
|
||||
|
||||
def deploy_skills(
|
||||
vault: Path,
|
||||
agent_key: str = "opencode",
|
||||
system_dir: str = "System",
|
||||
resources_dir: str = "Resources",
|
||||
literature_dir: str = "Literature",
|
||||
base_dir: str = "Bases",
|
||||
overwrite: bool = False,
|
||||
) -> dict:
|
||||
"""Deploy skills, commands, and AGENTS.md for a given agent platform.
|
||||
"""Deploy literature-qa skill and AGENTS.md to the vault.
|
||||
|
||||
Args:
|
||||
vault: Obsidian vault root
|
||||
agent_key: Agent platform key (opencode, cursor, claude, etc.)
|
||||
overwrite: True for update (overwrite existing), False for install (skip)
|
||||
vault: Obsidian vault root.
|
||||
agent_key: Agent platform key (opencode, claude, etc.).
|
||||
overwrite: If True, overwrite existing files (used by update).
|
||||
|
||||
Returns:
|
||||
dict with 'skills', 'commands', 'agents_md', 'errors' keys
|
||||
dict with 'skill_deployed', 'agents_md', 'errors' keys.
|
||||
"""
|
||||
agent_config = AGENT_CONFIGS.get(agent_key)
|
||||
if not agent_config:
|
||||
return {"skills": [], "commands": [], "agents_md": False, "errors": [f"Unknown agent: {agent_key}"]}
|
||||
|
||||
source_root = _resolve_source_root()
|
||||
if not (source_root / "skills" / "literature-qa").exists():
|
||||
return {"skills": [], "commands": [], "agents_md": False, "errors": ["Skills source not found in package"]}
|
||||
|
||||
skill_dir = agent_config.get("skill_dir", ".opencode/skills")
|
||||
fmt = agent_config.get("format", "skill_directory")
|
||||
prefix = agent_config.get("prefix", "/")
|
||||
imported_skills: list[str] = []
|
||||
errors: list[str] = []
|
||||
|
||||
# Deploy skills by format
|
||||
try:
|
||||
if fmt == "flat_command":
|
||||
imported_skills = _deploy_flat_command(vault, agent_config["command_dir"], source_root, system_dir, resources_dir, literature_dir, base_dir, skill_dir, overwrite)
|
||||
imported_skills += _deploy_skill_directory(vault, skill_dir, source_root, system_dir, resources_dir, literature_dir, base_dir, prefix, overwrite)
|
||||
elif fmt == "rules_file":
|
||||
imported_skills = _deploy_rules_file(vault, agent_config["skill_dir"], source_root, system_dir, resources_dir, literature_dir, base_dir, overwrite)
|
||||
else:
|
||||
imported_skills = _deploy_skill_directory(vault, skill_dir, source_root, system_dir, resources_dir, literature_dir, base_dir, prefix, overwrite)
|
||||
except Exception as e:
|
||||
errors.append(f"Skill deploy failed: {e}")
|
||||
# ── Deploy literature-qa skill ──
|
||||
skill_deployed = False
|
||||
source_root = _resolve_source_root()
|
||||
src_skill = source_root / "skills" / "literature-qa"
|
||||
|
||||
# Deploy AGENTS.md
|
||||
if src_skill.exists():
|
||||
skill_dir_name = AGENT_SKILL_DIRS.get(agent_key)
|
||||
if skill_dir_name:
|
||||
dst_skill = vault / skill_dir_name / "literature-qa"
|
||||
try:
|
||||
if overwrite and dst_skill.exists():
|
||||
shutil.rmtree(dst_skill, ignore_errors=True)
|
||||
dst_skill.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(src_skill, dst_skill, dirs_exist_ok=True)
|
||||
skill_deployed = True
|
||||
except Exception as e:
|
||||
errors.append(f"Skill deploy failed: {e}")
|
||||
else:
|
||||
errors.append(f"Unknown agent: {agent_key}")
|
||||
else:
|
||||
errors.append("Skills source not found in package")
|
||||
|
||||
# ── Deploy AGENTS.md ──
|
||||
agents_ok = False
|
||||
agents_src = source_root.parent / "AGENTS.md"
|
||||
if agents_src.exists():
|
||||
try:
|
||||
agents_dst = vault / "AGENTS.md"
|
||||
text = agents_src.read_text(encoding="utf-8")
|
||||
text = _substitute_vars(text, system_dir, resources_dir, literature_dir, base_dir, skill_dir, prefix)
|
||||
if overwrite and agents_dst.exists():
|
||||
agents_dst.unlink()
|
||||
if overwrite or not agents_dst.exists():
|
||||
agents_dst.write_text(text, encoding="utf-8")
|
||||
shutil.copy2(agents_src, agents_dst)
|
||||
agents_ok = True
|
||||
except Exception as e:
|
||||
errors.append(f"AGENTS.md deploy failed: {e}")
|
||||
|
||||
return {
|
||||
"skills": imported_skills,
|
||||
"commands": imported_skills,
|
||||
"skill_deployed": skill_deployed,
|
||||
"agents_md": agents_ok,
|
||||
"errors": errors,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
"""AgentInstaller — deploys skill files and agent configs."""
|
||||
"""AgentInstaller — deploys literature-qa skill to vault-local agent config."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
from paperforge.services.skill_deploy import AGENT_SKILL_DIRS
|
||||
from paperforge.setup import SetupStepResult
|
||||
|
||||
|
||||
class AgentInstaller:
|
||||
"""Deploy agent skill files, command files, and rules."""
|
||||
"""Deploy literature-qa skill directory to vault-local agent skills path."""
|
||||
|
||||
def __init__(self, vault: Path, agent_type: str = "opencode"):
|
||||
self.vault = vault
|
||||
|
|
@ -17,19 +18,12 @@ class AgentInstaller:
|
|||
self._script_dir = Path(__file__).resolve().parent.parent
|
||||
|
||||
def _get_skills_dir(self) -> Path:
|
||||
"""Get the target skills directory based on agent type."""
|
||||
if self.agent_type == "opencode":
|
||||
base = Path.home() / ".config" / "opencode"
|
||||
elif self.agent_type == "claude":
|
||||
base = Path.home() / ".claude"
|
||||
elif self.agent_type == "codex":
|
||||
base = Path.home() / ".codex"
|
||||
else:
|
||||
base = self.vault / ".agents"
|
||||
return base / "skills"
|
||||
"""Get the vault-local target skills directory."""
|
||||
skill_dir_name = AGENT_SKILL_DIRS.get(self.agent_type, ".agents/skills")
|
||||
return self.vault / skill_dir_name
|
||||
|
||||
def deploy_skills(self) -> SetupStepResult:
|
||||
"""Deploy literature-qa skill directory to agent config."""
|
||||
"""Deploy literature-qa skill as a single directory."""
|
||||
source_skills = self._script_dir / "skills" / "literature-qa"
|
||||
if not source_skills.exists():
|
||||
return SetupStepResult(
|
||||
|
|
@ -43,11 +37,7 @@ class AgentInstaller:
|
|||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
if source_skills.is_dir():
|
||||
shutil.copytree(source_skills, target_dir, dirs_exist_ok=True)
|
||||
else:
|
||||
shutil.copy2(source_skills, target_dir)
|
||||
|
||||
shutil.copytree(source_skills, target_dir, dirs_exist_ok=True)
|
||||
return SetupStepResult(
|
||||
step="agent_installer",
|
||||
ok=True,
|
||||
|
|
@ -62,42 +52,8 @@ class AgentInstaller:
|
|||
error=str(e),
|
||||
)
|
||||
|
||||
def deploy_commands(self) -> SetupStepResult:
|
||||
"""Deploy command files to vault agent config dir."""
|
||||
source_commands = self._script_dir / "command_files"
|
||||
if not source_commands.exists():
|
||||
return SetupStepResult(
|
||||
step="agent_installer",
|
||||
ok=True,
|
||||
message="No command files to deploy",
|
||||
details={"skipped": True},
|
||||
)
|
||||
|
||||
agent_dir = self.vault / ".agents"
|
||||
target_dir = agent_dir / "command_files"
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
for f in source_commands.iterdir():
|
||||
if f.is_file():
|
||||
shutil.copy2(f, target_dir / f.name)
|
||||
|
||||
return SetupStepResult(
|
||||
step="agent_installer",
|
||||
ok=True,
|
||||
message=f"Deployed command files to {target_dir}",
|
||||
details={"source": str(source_commands), "target": str(target_dir)},
|
||||
)
|
||||
except Exception as e:
|
||||
return SetupStepResult(
|
||||
step="agent_installer",
|
||||
ok=False,
|
||||
message="Failed to deploy command files",
|
||||
error=str(e),
|
||||
)
|
||||
|
||||
def deploy_agent_config(self) -> SetupStepResult:
|
||||
"""Deploy AGENTS.md and other agent config files."""
|
||||
"""Deploy AGENTS.md to vault root."""
|
||||
source_agents = self._script_dir.parent / "AGENTS.md"
|
||||
if not source_agents.exists():
|
||||
return SetupStepResult(
|
||||
|
|
@ -126,8 +82,4 @@ class AgentInstaller:
|
|||
|
||||
def run_all(self) -> list[SetupStepResult]:
|
||||
"""Run all deployment steps."""
|
||||
results = []
|
||||
results.append(self.deploy_skills())
|
||||
results.append(self.deploy_commands())
|
||||
results.append(self.deploy_agent_config())
|
||||
return results
|
||||
return [self.deploy_skills(), self.deploy_agent_config()]
|
||||
|
|
|
|||
|
|
@ -34,31 +34,6 @@ def _sync_obsidian_plugin(vault: Path) -> None:
|
|||
_pf_utils.install_obsidian_plugin(vault)
|
||||
|
||||
|
||||
def _deploy_skills(vault: Path) -> None:
|
||||
"""Copy the literature-qa skill from the package to the vault's agent dir."""
|
||||
import paperforge
|
||||
|
||||
src = Path(paperforge.__file__).parent / "skills" / "literature-qa"
|
||||
if not src.exists():
|
||||
logger.debug("Skills source not found: %s", src)
|
||||
return
|
||||
|
||||
raw = read_paperforge_json(vault)
|
||||
agent = raw.get("agent_platform", "opencode")
|
||||
agent_dirs = {
|
||||
"opencode": ".opencode/skills/literature-qa",
|
||||
"claude": ".claude/skills/literature-qa",
|
||||
"cursor": ".cursor/skills/literature-qa",
|
||||
"copilot": ".github/skills/literature-qa",
|
||||
"windsurf": ".windsurf/skills/literature-qa",
|
||||
"codex": ".codex/skills/literature-qa",
|
||||
}
|
||||
target_rel = agent_dirs.get(agent, agent_dirs["opencode"])
|
||||
dst = vault / target_rel
|
||||
shutil.copytree(src, dst, dirs_exist_ok=True)
|
||||
logger.info("Skills deployed: %s -> %s", src, dst)
|
||||
|
||||
|
||||
def protected_paths(vault: Path) -> set[str]:
|
||||
cfg = load_vault_config(vault)
|
||||
pf = f"{cfg['system_dir']}/PaperForge"
|
||||
|
|
@ -269,20 +244,10 @@ def _deploy_all_skills(vault: Path) -> None:
|
|||
from paperforge.config import load_vault_config
|
||||
|
||||
config = load_vault_config(vault)
|
||||
# Agent platform is a user preference, not a system default.
|
||||
# Fall back to opencode if not configured.
|
||||
agent_key = config.get("agent_platform") or "opencode"
|
||||
result = deploy_skills(
|
||||
vault=vault,
|
||||
agent_key=agent_key,
|
||||
system_dir=config.get("system_dir", "System"),
|
||||
resources_dir=config.get("resources_dir", "Resources"),
|
||||
literature_dir=config.get("literature_dir", "Literature"),
|
||||
base_dir=config.get("base_dir", "Bases"),
|
||||
overwrite=True,
|
||||
)
|
||||
if result["skills"]:
|
||||
logger.info("已部署 %d 个 skill: %s", len(result["skills"]), ", ".join(result["skills"]))
|
||||
result = deploy_skills(vault=vault, agent_key=agent_key, overwrite=True)
|
||||
if result["skill_deployed"]:
|
||||
logger.info("已部署 literature-qa skill")
|
||||
if result["agents_md"]:
|
||||
logger.info("已更新 AGENTS.md")
|
||||
for err in result.get("errors", []):
|
||||
|
|
|
|||
Loading…
Reference in a new issue