boost-skill-cli 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boost_cli/__init__.py +5 -0
- boost_cli/__main__.py +8 -0
- boost_cli/cli.py +213 -0
- boost_cli/commands/__init__.py +0 -0
- boost_cli/commands/configuration.py +1087 -0
- boost_cli/commands/discovery.py +650 -0
- boost_cli/commands/info.py +595 -0
- boost_cli/commands/intelligence.py +1048 -0
- boost_cli/commands/pkg.py +828 -0
- boost_cli/commands/quality.py +1041 -0
- boost_cli/commands/taps.py +203 -0
- boost_cli/commands/team.py +679 -0
- boost_cli/core/__init__.py +0 -0
- boost_cli/core/agents.py +33 -0
- boost_cli/core/ai.py +115 -0
- boost_cli/core/catalog.py +144 -0
- boost_cli/core/config.py +112 -0
- boost_cli/core/frontmatter.py +160 -0
- boost_cli/core/gitutil.py +69 -0
- boost_cli/core/journal.py +70 -0
- boost_cli/core/lockfile.py +116 -0
- boost_cli/core/output.py +99 -0
- boost_cli/core/paths.py +111 -0
- boost_cli/core/policy.py +63 -0
- boost_cli/core/registry.py +112 -0
- boost_cli/core/store.py +247 -0
- boost_cli/core/util.py +139 -0
- boost_cli/errors.py +11 -0
- boost_skill_cli-1.0.1.dist-info/METADATA +831 -0
- boost_skill_cli-1.0.1.dist-info/RECORD +34 -0
- boost_skill_cli-1.0.1.dist-info/WHEEL +5 -0
- boost_skill_cli-1.0.1.dist-info/entry_points.txt +2 -0
- boost_skill_cli-1.0.1.dist-info/licenses/LICENSE +675 -0
- boost_skill_cli-1.0.1.dist-info/top_level.txt +1 -0
boost_cli/core/store.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""The canonical store (~/.agents/skills) and agent symlinks.
|
|
2
|
+
|
|
3
|
+
install(): copy skill dir from a tap clone -> store, symlink into every
|
|
4
|
+
enabled agent dir, record in the lock file, log to the journal.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import shutil
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from ..errors import BoostError
|
|
14
|
+
from . import agents, journal, lockfile, paths, policy, registry, util
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class InstallResult:
|
|
19
|
+
name: str
|
|
20
|
+
dest: Path
|
|
21
|
+
linked: List[str] = field(default_factory=list)
|
|
22
|
+
conflicts: List[str] = field(default_factory=list)
|
|
23
|
+
score: int = 0
|
|
24
|
+
upgraded: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def skill_store_dir(name: str) -> Path:
|
|
28
|
+
return paths.store_dir() / name
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def installed() -> dict:
|
|
32
|
+
return lockfile.installed()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def source_dir_for(entry: dict) -> Path:
|
|
36
|
+
"""Absolute path of a catalog entry's skill dir inside its tap clone."""
|
|
37
|
+
tap = registry.get(entry["tap"])
|
|
38
|
+
src = tap.path if entry["rel_dir"] == "." else tap.path / entry["rel_dir"]
|
|
39
|
+
if not (src / "SKILL.md").exists():
|
|
40
|
+
raise BoostError("source for %s vanished from tap %s" % (entry["name"], tap.name),
|
|
41
|
+
hint="run `boost update %s`" % tap.name)
|
|
42
|
+
return src
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def link_agents(name: str, only: Optional[List[str]] = None) -> InstallResult:
|
|
46
|
+
"""Symlink store/<name> into each enabled agent dir. Returns result with
|
|
47
|
+
.linked (agent names) and .conflicts (paths that were real files/dirs)."""
|
|
48
|
+
res = InstallResult(name=name, dest=skill_store_dir(name))
|
|
49
|
+
target = skill_store_dir(name)
|
|
50
|
+
for agent, adir in agents.enabled_agents().items():
|
|
51
|
+
if only and agent not in only:
|
|
52
|
+
continue
|
|
53
|
+
adir.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
link = adir / name
|
|
55
|
+
if link.is_symlink():
|
|
56
|
+
link.unlink()
|
|
57
|
+
elif link.exists():
|
|
58
|
+
res.conflicts.append(str(link))
|
|
59
|
+
continue
|
|
60
|
+
link.symlink_to(target)
|
|
61
|
+
res.linked.append(agent)
|
|
62
|
+
return res
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def unlink_agents(name: str) -> List[str]:
|
|
66
|
+
removed = []
|
|
67
|
+
for agent, adir in agents.enabled_agents().items():
|
|
68
|
+
link = adir / name
|
|
69
|
+
if link.is_symlink():
|
|
70
|
+
link.unlink()
|
|
71
|
+
removed.append(agent)
|
|
72
|
+
return removed
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _copy_skill(src: Path, dest: Path) -> None:
|
|
76
|
+
if dest.exists():
|
|
77
|
+
shutil.rmtree(dest)
|
|
78
|
+
shutil.copytree(src, dest,
|
|
79
|
+
ignore=shutil.ignore_patterns(".git", "__pycache__", ".DS_Store"))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def install(entry: dict, force: bool = False,
|
|
83
|
+
only_agents: Optional[List[str]] = None) -> InstallResult:
|
|
84
|
+
"""Install a catalog entry. Raises BoostError on policy block or conflict."""
|
|
85
|
+
name = entry["name"]
|
|
86
|
+
existing = lockfile.get_skill(name)
|
|
87
|
+
if existing and existing.get("pinned") and not force:
|
|
88
|
+
raise BoostError("%s is pinned" % name, hint="`boost unpin %s` first" % name)
|
|
89
|
+
if existing and not force:
|
|
90
|
+
raise BoostError("%s is already installed (v%s)" % (name, existing.get("version")),
|
|
91
|
+
hint="`boost reinstall %s` to force, `boost update` to upgrade" % name)
|
|
92
|
+
|
|
93
|
+
violations = policy.check_install(entry, len(lockfile.installed()))
|
|
94
|
+
if violations:
|
|
95
|
+
raise BoostError("policy blocks installing %s: %s" % (name, "; ".join(violations)),
|
|
96
|
+
hint="inspect with `boost policy list`")
|
|
97
|
+
|
|
98
|
+
src = source_dir_for(entry)
|
|
99
|
+
dest = skill_store_dir(name)
|
|
100
|
+
paths.ensure_dirs()
|
|
101
|
+
_copy_skill(src, dest)
|
|
102
|
+
|
|
103
|
+
res = link_agents(name, only=only_agents)
|
|
104
|
+
res.upgraded = existing is not None
|
|
105
|
+
res.score, _ = util.score_skill(dest)
|
|
106
|
+
|
|
107
|
+
tap = registry.get(entry["tap"])
|
|
108
|
+
from . import gitutil
|
|
109
|
+
now = util.now_iso()
|
|
110
|
+
lockfile.set_skill(name, {
|
|
111
|
+
"version": entry.get("version", "0.0.0"),
|
|
112
|
+
"tap": entry["tap"],
|
|
113
|
+
"source_dir": entry.get("rel_dir", "."),
|
|
114
|
+
"commit": gitutil.head_commit(tap.path),
|
|
115
|
+
"sha256": util.sha256_dir(dest),
|
|
116
|
+
"installed_at": (existing or {}).get("installed_at", now),
|
|
117
|
+
"updated_at": now,
|
|
118
|
+
"pinned": bool((existing or {}).get("pinned")),
|
|
119
|
+
"quarantined": False,
|
|
120
|
+
"agents": res.linked,
|
|
121
|
+
"tags": (existing or {}).get("tags", []),
|
|
122
|
+
})
|
|
123
|
+
journal.log("install", name, tap=entry["tap"], version=entry.get("version"))
|
|
124
|
+
return res
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def install_from_path(src_dir: Path, name: Optional[str] = None,
|
|
128
|
+
tap_label: str = "local",
|
|
129
|
+
only_agents: Optional[List[str]] = None) -> InstallResult:
|
|
130
|
+
"""Install directly from a local directory (used by `boost import`)."""
|
|
131
|
+
src_dir = Path(src_dir)
|
|
132
|
+
if not (src_dir / "SKILL.md").exists():
|
|
133
|
+
raise BoostError("%s has no SKILL.md" % src_dir)
|
|
134
|
+
from . import frontmatter
|
|
135
|
+
meta, _ = frontmatter.parse((src_dir / "SKILL.md").read_text(
|
|
136
|
+
encoding="utf-8", errors="replace"))
|
|
137
|
+
name = name or str(meta.get("name") or src_dir.name)
|
|
138
|
+
dest = skill_store_dir(name)
|
|
139
|
+
paths.ensure_dirs()
|
|
140
|
+
_copy_skill(src_dir, dest)
|
|
141
|
+
res = link_agents(name, only=only_agents)
|
|
142
|
+
res.score, _ = util.score_skill(dest)
|
|
143
|
+
now = util.now_iso()
|
|
144
|
+
existing = lockfile.get_skill(name)
|
|
145
|
+
lockfile.set_skill(name, {
|
|
146
|
+
"version": str(meta.get("version") or "0.0.0"),
|
|
147
|
+
"tap": tap_label,
|
|
148
|
+
"source_dir": str(src_dir),
|
|
149
|
+
"commit": "",
|
|
150
|
+
"sha256": util.sha256_dir(dest),
|
|
151
|
+
"installed_at": (existing or {}).get("installed_at", now),
|
|
152
|
+
"updated_at": now,
|
|
153
|
+
"pinned": False,
|
|
154
|
+
"quarantined": False,
|
|
155
|
+
"agents": res.linked,
|
|
156
|
+
"tags": (existing or {}).get("tags", []),
|
|
157
|
+
})
|
|
158
|
+
journal.log("import", name, source=str(src_dir))
|
|
159
|
+
return res
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def uninstall(name: str) -> dict:
|
|
163
|
+
entry = lockfile.get_skill(name)
|
|
164
|
+
if not entry:
|
|
165
|
+
raise BoostError("%s is not installed" % name,
|
|
166
|
+
hint="see what is with `boost list`")
|
|
167
|
+
removed_links = unlink_agents(name)
|
|
168
|
+
dest = skill_store_dir(name)
|
|
169
|
+
if dest.exists():
|
|
170
|
+
shutil.rmtree(dest)
|
|
171
|
+
lockfile.remove_skill(name)
|
|
172
|
+
journal.log("uninstall", name)
|
|
173
|
+
return {"name": name, "unlinked": removed_links, "entry": entry}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def sync_plan() -> Dict[str, list]:
|
|
177
|
+
"""Compare lock file <-> store <-> agent symlinks.
|
|
178
|
+
|
|
179
|
+
Returns {missing_store, missing_links, stale_links, orphaned_store}
|
|
180
|
+
missing_store: lock entries whose store dir is gone
|
|
181
|
+
missing_links: (skill, agent) pairs that should be linked but aren't
|
|
182
|
+
stale_links: paths in agent dirs that are broken/unmanaged symlinks
|
|
183
|
+
orphaned_store: store dirs not present in the lock file
|
|
184
|
+
"""
|
|
185
|
+
lock = lockfile.installed()
|
|
186
|
+
plan: dict[str, list] = {"missing_store": [], "missing_links": [],
|
|
187
|
+
"stale_links": [], "orphaned_store": []}
|
|
188
|
+
for name, entry in lock.items():
|
|
189
|
+
sdir = skill_store_dir(name)
|
|
190
|
+
if not sdir.is_dir():
|
|
191
|
+
plan["missing_store"].append(name)
|
|
192
|
+
continue
|
|
193
|
+
if entry.get("quarantined"):
|
|
194
|
+
continue
|
|
195
|
+
for agent, adir in agents.enabled_agents().items():
|
|
196
|
+
link = adir / name
|
|
197
|
+
if not link.is_symlink() or not link.exists():
|
|
198
|
+
plan["missing_links"].append((name, agent))
|
|
199
|
+
store_root = paths.store_dir()
|
|
200
|
+
if store_root.is_dir():
|
|
201
|
+
for child in store_root.iterdir():
|
|
202
|
+
if child.is_dir() and child.name not in lock:
|
|
203
|
+
plan["orphaned_store"].append(child.name)
|
|
204
|
+
for agent, adir in agents.enabled_agents().items():
|
|
205
|
+
if not adir.is_dir():
|
|
206
|
+
continue
|
|
207
|
+
for link in adir.iterdir():
|
|
208
|
+
if link.is_symlink():
|
|
209
|
+
points_into_store = str(paths.store_dir()) in str(
|
|
210
|
+
link.resolve() if link.exists() else link.readlink())
|
|
211
|
+
if not link.exists() or (points_into_store and link.name not in lock):
|
|
212
|
+
plan["stale_links"].append(str(link))
|
|
213
|
+
return plan
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def sync_apply(plan: Dict[str, list]) -> List[str]:
|
|
217
|
+
"""Fix what sync_plan found. Returns human-readable actions taken."""
|
|
218
|
+
actions = []
|
|
219
|
+
for name, agent in plan["missing_links"]:
|
|
220
|
+
res = link_agents(name, only=[agent])
|
|
221
|
+
if agent in res.linked:
|
|
222
|
+
actions.append("linked %s → %s" % (name, agent))
|
|
223
|
+
for path in plan["stale_links"]:
|
|
224
|
+
p = Path(path)
|
|
225
|
+
if p.is_symlink():
|
|
226
|
+
p.unlink()
|
|
227
|
+
actions.append("removed stale link %s" % path)
|
|
228
|
+
for name in plan["missing_store"]:
|
|
229
|
+
entry = lockfile.get_skill(name) or {}
|
|
230
|
+
tap_name = entry.get("tap")
|
|
231
|
+
restored = False
|
|
232
|
+
if tap_name and tap_name != "local":
|
|
233
|
+
try:
|
|
234
|
+
from . import catalog
|
|
235
|
+
matches = [e for e in catalog.find(name) if e["tap"] == tap_name]
|
|
236
|
+
if matches:
|
|
237
|
+
install(matches[0], force=True)
|
|
238
|
+
actions.append("reinstalled missing %s from %s" % (name, tap_name))
|
|
239
|
+
restored = True
|
|
240
|
+
except BoostError:
|
|
241
|
+
pass
|
|
242
|
+
if not restored:
|
|
243
|
+
lockfile.remove_skill(name)
|
|
244
|
+
actions.append("dropped %s from lock (store dir missing, source gone)" % name)
|
|
245
|
+
if actions:
|
|
246
|
+
journal.log("sync", "%d fixes" % len(actions))
|
|
247
|
+
return actions
|
boost_cli/core/util.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Small shared utilities: time, hashing, versions, skill quality scoring."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import List, Tuple
|
|
9
|
+
|
|
10
|
+
IGNORED = {".git", "__pycache__", ".DS_Store"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def now_iso() -> str:
|
|
14
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def rel_time(iso: str) -> str:
|
|
18
|
+
"""'2026-07-16T01:00:00Z' -> '3h ago' (best effort)."""
|
|
19
|
+
try:
|
|
20
|
+
then = datetime.strptime(iso, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
|
21
|
+
except (ValueError, TypeError):
|
|
22
|
+
return iso or "?"
|
|
23
|
+
secs = (datetime.now(timezone.utc) - then).total_seconds()
|
|
24
|
+
for limit, size, unit in ((60, 1, "s"), (3600, 60, "m"),
|
|
25
|
+
(86400, 3600, "h"), (604800, 86400, "d")):
|
|
26
|
+
if secs < limit:
|
|
27
|
+
return "%d%s ago" % (max(1, secs // size), unit)
|
|
28
|
+
if secs < 604800 * 8:
|
|
29
|
+
return "%dw ago" % (secs // 604800)
|
|
30
|
+
return then.strftime("%Y-%m-%d")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def human_size(n: int) -> str:
|
|
34
|
+
size = float(n)
|
|
35
|
+
for unit in ("B", "KB", "MB", "GB"):
|
|
36
|
+
if size < 1024 or unit == "GB":
|
|
37
|
+
return ("%d%s" if unit == "B" else "%.1f%s") % (size, unit)
|
|
38
|
+
size /= 1024.0
|
|
39
|
+
return str(size)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def slugify(name: str) -> str:
|
|
43
|
+
return re.sub(r"[^a-z0-9-]+", "-", name.strip().lower()).strip("-") or "skill"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def sha256_dir(path: Path) -> str:
|
|
47
|
+
"""Deterministic content hash of a directory tree (paths + bytes)."""
|
|
48
|
+
h = hashlib.sha256()
|
|
49
|
+
root = Path(path)
|
|
50
|
+
files = sorted(
|
|
51
|
+
p for p in root.rglob("*")
|
|
52
|
+
if p.is_file() and not any(part in IGNORED for part in p.parts)
|
|
53
|
+
)
|
|
54
|
+
for f in files:
|
|
55
|
+
h.update(str(f.relative_to(root)).encode())
|
|
56
|
+
h.update(b"\0")
|
|
57
|
+
h.update(f.read_bytes())
|
|
58
|
+
h.update(b"\0")
|
|
59
|
+
return h.hexdigest()
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def dir_size(path: Path) -> int:
|
|
63
|
+
return sum(p.stat().st_size for p in Path(path).rglob("*") if p.is_file())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def semver_tuple(v: str):
|
|
67
|
+
parts = re.findall(r"\d+", str(v or "0"))[:3]
|
|
68
|
+
return tuple(int(p) for p in parts) + (0,) * (3 - len(parts))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def semver_gt(a: str, b: str) -> bool:
|
|
72
|
+
return semver_tuple(a) > semver_tuple(b)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def score_skill(skill_dir: Path) -> Tuple[int, List[str]]:
|
|
76
|
+
"""Heuristic quality score 0-100 for a skill directory, with notes.
|
|
77
|
+
|
|
78
|
+
Shared by `boost install` (display), `boost lint`, and `boost test`.
|
|
79
|
+
"""
|
|
80
|
+
from . import frontmatter
|
|
81
|
+
|
|
82
|
+
skill_md = Path(skill_dir) / "SKILL.md"
|
|
83
|
+
notes: List[str] = []
|
|
84
|
+
if not skill_md.exists():
|
|
85
|
+
return 0, ["missing SKILL.md"]
|
|
86
|
+
try:
|
|
87
|
+
text = skill_md.read_text(encoding="utf-8", errors="replace")
|
|
88
|
+
except OSError as e:
|
|
89
|
+
return 0, ["unreadable SKILL.md: %s" % e]
|
|
90
|
+
meta, body = frontmatter.parse(text)
|
|
91
|
+
score = 20 # exists and parses
|
|
92
|
+
|
|
93
|
+
if meta.get("name"):
|
|
94
|
+
score += 10
|
|
95
|
+
else:
|
|
96
|
+
notes.append("frontmatter missing `name`")
|
|
97
|
+
desc = str(meta.get("description") or "")
|
|
98
|
+
if desc:
|
|
99
|
+
score += 10
|
|
100
|
+
if len(desc) >= 40:
|
|
101
|
+
score += 5
|
|
102
|
+
else:
|
|
103
|
+
notes.append("description is thin (<40 chars)")
|
|
104
|
+
else:
|
|
105
|
+
notes.append("frontmatter missing `description`")
|
|
106
|
+
if meta.get("version"):
|
|
107
|
+
score += 10
|
|
108
|
+
if not re.match(r"^\d+\.\d+(\.\d+)?", str(meta["version"])):
|
|
109
|
+
score -= 5
|
|
110
|
+
notes.append("version is not semver-ish")
|
|
111
|
+
else:
|
|
112
|
+
notes.append("frontmatter missing `version`")
|
|
113
|
+
|
|
114
|
+
if len(body.strip()) >= 200:
|
|
115
|
+
score += 15
|
|
116
|
+
else:
|
|
117
|
+
notes.append("body is short (<200 chars)")
|
|
118
|
+
if re.search(r"^#{1,3} ", body, re.M):
|
|
119
|
+
score += 10
|
|
120
|
+
else:
|
|
121
|
+
notes.append("no markdown headings in body")
|
|
122
|
+
if "```" in body or re.search(r"^\d+\. ", body, re.M) or re.search(r"^- ", body, re.M):
|
|
123
|
+
score += 10 # concrete steps or examples
|
|
124
|
+
else:
|
|
125
|
+
notes.append("no examples, steps, or code blocks")
|
|
126
|
+
if not re.search(r"\bTODO\b|\bFIXME\b", body):
|
|
127
|
+
score += 5
|
|
128
|
+
else:
|
|
129
|
+
notes.append("contains TODO/FIXME")
|
|
130
|
+
if meta.get("license") or (Path(skill_dir) / "LICENSE").exists():
|
|
131
|
+
score += 5
|
|
132
|
+
extras = [p for p in Path(skill_dir).iterdir()
|
|
133
|
+
if p.name not in IGNORED and p.name != "SKILL.md"]
|
|
134
|
+
if extras:
|
|
135
|
+
score += 5 # ships supporting references/scripts
|
|
136
|
+
if len(text) > 48_000:
|
|
137
|
+
score -= 10
|
|
138
|
+
notes.append("very large SKILL.md (>48KB) — consider splitting")
|
|
139
|
+
return max(0, min(100, score)), notes
|
boost_cli/errors.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""User-facing errors."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BoostError(Exception):
|
|
6
|
+
"""An expected, user-facing failure. Printed without a traceback."""
|
|
7
|
+
|
|
8
|
+
def __init__(self, message: str, hint: str | None = None):
|
|
9
|
+
super().__init__(message)
|
|
10
|
+
self.message = message
|
|
11
|
+
self.hint = hint
|