-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Expand file tree
/
Copy pathinstall_opencode.py
More file actions
162 lines (134 loc) · 5.06 KB
/
Copy pathinstall_opencode.py
File metadata and controls
162 lines (134 loc) · 5.06 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
#!/usr/bin/env python3
"""Install generated OpenCode artifacts into the user's OpenCode config directory."""
from __future__ import annotations
import argparse
import os
from dataclasses import dataclass, field
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
GENERATED_ROOT = REPO_ROOT / ".opencode"
ARTIFACT_GLOBS = {
"agents": "*.md",
"commands": "*.md",
"skills": "*",
}
@dataclass
class InstallReport:
linked: int = 0
unchanged: int = 0
removed: int = 0
skipped: int = 0
errors: list[str] = field(default_factory=list)
@property
def ok(self) -> bool:
return not self.errors
def default_config_dir(env: dict[str, str] | None = None) -> Path:
env = env or os.environ
if env.get("OPENCODE_CONFIG_DIR"):
return Path(env["OPENCODE_CONFIG_DIR"]).expanduser()
if env.get("XDG_CONFIG_HOME"):
return Path(env["XDG_CONFIG_HOME"]).expanduser() / "opencode"
return Path.home() / ".config" / "opencode"
def _is_relative_to(child: Path, parent: Path) -> bool:
try:
child.resolve(strict=False).relative_to(parent.resolve(strict=False))
except ValueError:
return False
return True
def _generated_artifacts(repo_root: Path) -> list[tuple[str, Path]]:
generated_root = repo_root / ".opencode"
artifacts: list[tuple[str, Path]] = []
for subdir, pattern in ARTIFACT_GLOBS.items():
source_dir = generated_root / subdir
if not source_dir.is_dir():
raise FileNotFoundError(
f"{source_dir} does not exist; run `make generate HARNESS=opencode` first"
)
for src in sorted(source_dir.glob(pattern)):
if subdir == "skills" and not src.is_dir():
continue
if subdir != "skills" and not src.is_file():
continue
artifacts.append((subdir, src.resolve()))
return artifacts
def _link_one(src: Path, dst: Path, *, force: bool, report: InstallReport) -> None:
if dst.is_symlink():
if dst.resolve(strict=False) == src:
report.unchanged += 1
return
if not force:
report.errors.append(
f"{dst} already exists as a symlink to {dst.resolve(strict=False)}; "
"rerun with FORCE=1 to replace symlink conflicts"
)
return
dst.unlink()
elif dst.exists():
report.errors.append(f"{dst} already exists and is not a symlink; refusing to overwrite")
return
dst.parent.mkdir(parents=True, exist_ok=True)
dst.symlink_to(src, target_is_directory=src.is_dir())
report.linked += 1
def install(
*,
repo_root: Path = REPO_ROOT,
config_dir: Path | None = None,
force: bool = False,
) -> InstallReport:
config_dir = (config_dir or default_config_dir()).expanduser()
report = InstallReport()
try:
artifacts = _generated_artifacts(repo_root)
except FileNotFoundError as exc:
report.errors.append(str(exc))
return report
for subdir, src in artifacts:
dst = config_dir / subdir / src.name
_link_one(src, dst, force=force, report=report)
return report
def uninstall(
*,
repo_root: Path = REPO_ROOT,
config_dir: Path | None = None,
) -> InstallReport:
config_dir = (config_dir or default_config_dir()).expanduser()
generated_root = (repo_root / ".opencode").resolve(strict=False)
report = InstallReport()
for subdir in ARTIFACT_GLOBS:
target_dir = config_dir / subdir
if not target_dir.is_dir():
continue
for dst in sorted(target_dir.iterdir()):
if not dst.is_symlink():
report.skipped += 1
continue
target = dst.resolve(strict=False)
if _is_relative_to(target, generated_root):
dst.unlink()
report.removed += 1
else:
report.skipped += 1
return report
def _print_report(action: str, config_dir: Path, report: InstallReport) -> None:
print(
f"{action}: config={config_dir} linked={report.linked} unchanged={report.unchanged} "
f"removed={report.removed} skipped={report.skipped}"
)
for error in report.errors:
print(f"error: {error}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("action", choices=("install", "uninstall"))
parser.add_argument("--config-dir", type=Path, default=None)
parser.add_argument("--repo-root", type=Path, default=REPO_ROOT)
parser.add_argument("--force", action="store_true", help="Replace conflicting symlinks only")
args = parser.parse_args()
config_dir = (args.config_dir or default_config_dir()).expanduser()
if args.action == "install":
report = install(repo_root=args.repo_root, config_dir=config_dir, force=args.force)
else:
report = uninstall(repo_root=args.repo_root, config_dir=config_dir)
_print_report(args.action, config_dir, report)
return 0 if report.ok else 1
if __name__ == "__main__":
raise SystemExit(main())