-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.py
More file actions
executable file
·242 lines (199 loc) · 9.04 KB
/
Copy pathinstall.py
File metadata and controls
executable file
·242 lines (199 loc) · 9.04 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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
#!/usr/bin/env python3
"""Cross-platform installer for the kernel-radar skill.
kernel-radar is one tool-agnostic Python engine (engine/) plus a canonical
instruction file (SKILL.md). This installer:
1. copies the engine to a shared home ($KERNEL_RADAR_HOME, default ~/.kernel-radar);
2. for each selected agent tool, renders that tool's manifest from the ONE
canonical SKILL.md (frontmatter re-wrapped per tool, {{ENGINE}} resolved to
the real path) and writes it where the tool looks for it.
Supported targets: claude, codex, opencode, cursor (and `all`).
The engine is stdlib-only (Python 3.8+), so there is nothing to compile and no
third-party package to install. Each developer runs this once per machine.
Usage:
python3 install.py --target claude
python3 install.py --target claude,codex,opencode,cursor # or: --target all
python3 install.py --target cursor --cursor-dir /path/to/project/.cursor/rules
python3 install.py --list # show resolved paths, write nothing
python3 install.py --uninstall --target all
"""
import argparse
import os
import re
import shutil
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
CANONICAL = os.path.join(HERE, "SKILL.md")
OPENAI_YAML = os.path.join(HERE, "adapters", "openai.yaml")
SRC_ENGINE = os.path.join(HERE, "engine")
ALL_TARGETS = ["claude", "codex", "opencode", "cursor"]
def version():
try:
with open(os.path.join(HERE, "VERSION"), encoding="utf-8") as f:
return f.read().strip()
except OSError:
return "0.0.0"
def home():
"""Shared engine home. Override with $KERNEL_RADAR_HOME."""
return os.environ.get("KERNEL_RADAR_HOME") or os.path.join(
os.path.expanduser("~"), ".kernel-radar")
def engine_dir():
return os.path.join(home(), "engine")
def split_frontmatter(text):
"""Return (frontmatter_dict_subset, body). Only `name`/`description` are read
(simple `key: value` lines); the rest of the body is returned verbatim."""
m = re.match(r"^---\n(.*?)\n---\n?(.*)$", text, re.DOTALL)
if not m:
return {}, text
meta = {}
for line in m.group(1).splitlines():
km = re.match(r"^(\w+):\s*(.*)$", line)
if km:
meta[km.group(1)] = km.group(2).strip()
return meta, m.group(2).lstrip("\n")
def default_dirs(cur_cursor_dir):
h = os.path.expanduser("~")
return {
# Claude Code: a self-contained skill folder (SKILL.md references the
# shared engine by absolute path, so the scripts need not be duplicated).
"claude": os.path.join(h, ".claude", "skills", "kernel-radar"),
# Codex: skill folder + its agents manifest.
"codex": os.path.join(h, ".codex", "skills", "kernel-radar"),
# opencode: a global slash-command file.
"opencode": os.path.join(h, ".config", "opencode", "command"),
# Cursor rules are PROJECT-scoped; default to the current project's
# .cursor/rules (override with --cursor-dir for a different project).
"cursor": cur_cursor_dir or os.path.join(os.getcwd(), ".cursor", "rules"),
}
# Per-tool frontmatter wrapper around the shared instruction body.
def render(target, meta, body):
desc = meta.get("description", "kernel-radar roadmap & competitor radar")
if target == "claude":
# Claude Code supports a hard opt-in gate in addition to the narrowly
# scoped description shared by all targets.
fm = (f"---\nname: kernel-radar\ndescription: {desc}\n"
"disable-model-invocation: true\n---\n\n")
elif target == "codex":
fm = f"---\nname: kernel-radar\ndescription: {desc}\n---\n\n"
elif target == "opencode":
fm = f"---\ndescription: {desc}\n---\n\n"
elif target == "cursor":
fm = f"---\ndescription: {desc}\nalwaysApply: false\n---\n\n"
else:
fm = ""
return fm + body
def target_files(target, dirs):
"""Where each target's rendered file(s) land. Returns list of (path, kind)."""
d = dirs[target]
if target in ("claude", "codex"):
return [(os.path.join(d, "SKILL.md"), "skill")]
if target == "opencode":
return [(os.path.join(d, "kernel-radar.md"), "command")]
if target == "cursor":
return [(os.path.join(d, "kernel-radar.mdc"), "rule")]
return []
def copy_engine(dry):
dst = engine_dir()
print(f" engine -> {dst}")
if dry:
return
if os.path.isdir(dst):
shutil.rmtree(dst)
shutil.copytree(SRC_ENGINE, dst,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
def install_target(target, meta, body, dirs, dry):
for path, _ in target_files(target, dirs):
print(f" {target:9}-> {path}")
if dry:
continue
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(render(target, meta, body))
# Codex also wants its agents manifest beside the skill.
if target == "codex" and not dry and os.path.exists(OPENAI_YAML):
agents_dir = os.path.join(dirs["codex"], "agents")
os.makedirs(agents_dir, exist_ok=True)
shutil.copyfile(OPENAI_YAML, os.path.join(agents_dir, "openai.yaml"))
def uninstall_target(target, dirs, dry):
d = dirs[target]
if target in ("claude", "codex"):
print(f" remove {d}")
if not dry and os.path.isdir(d):
shutil.rmtree(d)
else:
for path, _ in target_files(target, dirs):
print(f" remove {path}")
if not dry and os.path.exists(path):
os.remove(path)
def parse_targets(raw):
picked = []
for chunk in raw:
for t in chunk.split(","):
t = t.strip().lower()
if t == "all":
picked = list(ALL_TARGETS)
elif t in ALL_TARGETS:
if t not in picked:
picked.append(t)
elif t:
sys.exit(f"unknown target: {t!r} (choose from {ALL_TARGETS} or 'all')")
return picked
def invoke_hint(target):
return {
"claude": "In Claude Code: ask for “kernel-radar” or run /kernel-radar.",
"codex": "In Codex: invoke the kernel-radar skill.",
"opencode": "In opencode: run /kernel-radar.",
"cursor": "In Cursor: the rule auto-attaches; ask the agent to run kernel-radar.",
}[target]
def main():
ap = argparse.ArgumentParser(description="Install the kernel-radar skill for one or more agent tools.")
ap.add_argument("--target", action="append", default=[],
help="claude | codex | opencode | cursor | all (comma-separated or repeated).")
ap.add_argument("--claude-dir", default=None)
ap.add_argument("--codex-dir", default=None)
ap.add_argument("--opencode-dir", default=None)
ap.add_argument("--cursor-dir", default=None,
help="Cursor rules dir (default: ./.cursor/rules in the current project).")
ap.add_argument("--list", action="store_true", help="Show resolved paths and exit without writing.")
ap.add_argument("--uninstall", action="store_true")
ap.add_argument("--dry-run", action="store_true")
ap.add_argument("--version", action="store_true")
args = ap.parse_args()
if args.version:
print(f"kernel-radar {version()}")
return 0
targets = parse_targets(args.target) or (ALL_TARGETS if args.list else [])
if not targets:
ap.error("nothing to do: pass --target claude|codex|opencode|cursor|all")
dirs = default_dirs(args.cursor_dir)
for key, override in (("claude", args.claude_dir), ("codex", args.codex_dir),
("opencode", args.opencode_dir), ("cursor", args.cursor_dir)):
if override:
dirs[key] = os.path.abspath(override)
dry = args.dry_run or args.list
print(f"kernel-radar {version()} · targets: {', '.join(targets)}"
f"{' · DRY RUN' if dry else ''}\n")
if args.uninstall:
for t in targets:
uninstall_target(t, dirs, dry)
print("\nNote: the shared engine at", engine_dir(),
"was left in place (delete it manually if no tool still uses it).")
return 0
meta, raw_body = split_frontmatter(open(CANONICAL, encoding="utf-8").read())
body = raw_body.replace("{{ENGINE}}", engine_dir()).replace("{{VERSION}}", version())
copy_engine(dry)
for t in targets:
install_target(t, meta, body, dirs, dry)
if not dry:
print("\nDone. Next steps:")
if not (os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")):
print(" • Set a GitHub token for full accuracy:")
print(" export GITHUB_TOKEN=<fine-grained token, public-repo read-only>")
for t in targets:
print(f" • {invoke_hint(t)}")
print(f"\n Reports are written under <project>/kernel-radar-reports/ at run time.")
if "cursor" in targets:
print(f" Cursor note: the rule was placed in {dirs['cursor']} "
f"(project-scoped). Re-run with --cursor-dir for another project.")
return 0
if __name__ == "__main__":
sys.exit(main())