feat: install into Claude Code, opencode or Hermes from one script

opencode reads the same SKILL.md files but needs its own subagent format and a
TypeScript plugin for the guard, formatter and gate, so install.py converts the
agents and points the copies at this checkout.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
EvandroandClaude Opus 5.5 committed 2026-09-24 21:52:06 -03:00
1 parent f2db2d69da
commit 29b5ebf400
2 files changed
+133

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
"""Install dev-workflows into Claude Code, opencode or Hermes Agent.
python3 scripts/install.py claude # plugin via local marketplace (skills, agents, hooks)
python3 scripts/install.py opencode # skills + agents + plugin under ~/.config/opencode
python3 scripts/install.py hermes # skills under ~/.hermes/skills (no agents/hooks there)
opencode and Hermes do not substitute ${CLAUDE_PLUGIN_ROOT}, so the copies point at this checkout:
keep it where it is, or re-run the install after moving it.
"""
import argparse, os, re, shutil, subprocess, sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
MARKER = ".dev-workflows-install"
TEXT_EXT = {".md", ".txt", ".json", ".toml", ".yaml", ".yml"}
def copy_tree(src, dst, force):
if os.path.exists(dst) and not os.path.exists(os.path.join(dst, MARKER)) and not force:
sys.exit(f"{dst} já existe e não foi instalado por este script. Use --force para sobrescrever.")
shutil.rmtree(dst, ignore_errors=True)
shutil.copytree(src, dst)
for dirpath, _, names in os.walk(dst):
for n in names:
if os.path.splitext(n)[1] in TEXT_EXT:
p = os.path.join(dirpath, n)
text = open(p, encoding="utf-8", errors="ignore").read()
if "${CLAUDE_PLUGIN_ROOT}" in text:
open(p, "w", encoding="utf-8").write(text.replace("${CLAUDE_PLUGIN_ROOT}", ROOT))
open(os.path.join(dst, MARKER), "w").write(ROOT + "\n")
def install_skills(target_dir, force):
os.makedirs(target_dir, exist_ok=True)
names = sorted(d for d in os.listdir(os.path.join(ROOT, "skills"))
if os.path.isfile(os.path.join(ROOT, "skills", d, "SKILL.md")))
for name in names:
copy_tree(os.path.join(ROOT, "skills", name), os.path.join(target_dir, name), force)
print(f"{len(names)} skills em {target_dir}")
def opencode_agent(path):
"""Claude Code agent frontmatter -> opencode subagent frontmatter."""
text = open(path, encoding="utf-8").read()
m = re.match(r"---\n(.*?)\n---\n(.*)", text, re.S)
meta = dict(re.findall(r"^(\w+):\s*(.*)$", m.group(1), re.M))
tools = {t.strip() for t in meta.get("tools", "").split(",")}
skills = re.findall(r"[\w-]+", meta.get("skills", ""))
perm = {"edit": "allow" if tools & {"Edit", "Write"} else "deny",
"bash": "allow" if "Bash" in tools else "deny",
"webfetch": "allow" if "WebFetch" in tools else "deny"}
body = m.group(2).replace("${CLAUDE_PLUGIN_ROOT}", ROOT)
if skills:
body = f"Antes de começar, carregue a(s) skill(s): {', '.join(skills)}.\n\n" + body
head = ["# dev-workflows", f"description: {meta['description']}", "mode: subagent", "permission:"]
head += [f" {k}: {v}" for k, v in perm.items()]
return "---\n" + "\n".join(head) + "\n---\n\n" + body
def install_opencode(force):
base = os.path.expanduser("~/.config/opencode")
install_skills(os.path.join(base, "skills"), force)
agents_dir = os.path.join(base, "agents")
os.makedirs(agents_dir, exist_ok=True)
for f in sorted(os.listdir(os.path.join(ROOT, "agents"))):
dst = os.path.join(agents_dir, f)
if os.path.exists(dst) and "dev-workflows" not in open(dst, encoding="utf-8").read() and not force:
sys.exit(f"{dst} já existe e não é deste pacote. Use --force para sobrescrever.")
open(dst, "w", encoding="utf-8").write(opencode_agent(os.path.join(ROOT, "agents", f)))
plugins_dir = os.path.join(base, "plugins")
os.makedirs(plugins_dir, exist_ok=True)
plugin = open(os.path.join(ROOT, "adapters", "opencode", "dev-workflows.ts"), encoding="utf-8").read()
open(os.path.join(plugins_dir, "dev-workflows.ts"), "w", encoding="utf-8").write(plugin.replace("__DW_ROOT__", ROOT))
print(f"agentes em {agents_dir}, plugin em {plugins_dir}/dev-workflows.ts")
def install_claude():
for cmd in (["claude", "plugin", "marketplace", "add", ROOT], ["claude", "plugin", "install", "dev-workflows@dev-workflows"]):
print("$ " + " ".join(cmd))
if subprocess.run(cmd).returncode != 0:
sys.exit("falhou; rode o comando acima manualmente para ver o erro")
def main():
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("target", choices=["claude", "opencode", "hermes"])
ap.add_argument("--force", action="store_true", help="sobrescreve pastas com o mesmo nome que não vieram deste script")
a = ap.parse_args()
if a.target == "claude":
install_claude()
elif a.target == "opencode":
install_opencode(a.force)
else:
install_skills(os.path.expanduser("~/.hermes/skills"), a.force)
print("Hermes: agentes e hooks não se aplicam; rode o gate manualmente.")
if __name__ == "__main__":
main()