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.
@@ -0,0 +1,203 @@
1
+ """Registry (taps) commands: tap, untap, taps, outdated."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+
8
+ from ..core import catalog, config, gitutil, journal, lockfile, paths, registry, store, util
9
+ from ..core import output as out
10
+ from ..errors import BoostError
11
+
12
+
13
+ def _tilde(p) -> str:
14
+ """Contract $HOME to ~ in a path-ish string for display."""
15
+ s = str(p)
16
+ for h in {str(paths.home()), str(paths.home().resolve())}:
17
+ if s == h:
18
+ return "~"
19
+ if s.startswith(h + os.sep):
20
+ return "~" + s[len(h):]
21
+ return s
22
+
23
+
24
+ def cmd_tap(argv) -> int:
25
+ """boost tap [SPEC] [--defaults] [--curated]"""
26
+ p = argparse.ArgumentParser(
27
+ prog="boost tap",
28
+ description="Add a GitHub repo as a skill registry")
29
+ p.add_argument("spec", nargs="?",
30
+ help="owner/repo, a git URL, or a local directory")
31
+ p.add_argument("--defaults", action="store_true",
32
+ help="tap the recommended public registries")
33
+ p.add_argument("--curated", action="store_true",
34
+ help="mark the tap as curated (★ in listings)")
35
+ args = p.parse_args(argv)
36
+ if not args.spec and not args.defaults:
37
+ p.error("provide a SPEC or --defaults")
38
+
39
+ rc = 0
40
+ if args.defaults:
41
+ existing = {t.name for t in registry.list_taps()}
42
+ for default in config.DEFAULT_TAPS:
43
+ if default["name"] in existing:
44
+ out.info(out.c("%s already tapped" % default["name"], out.DIM))
45
+ continue
46
+ try:
47
+ tap = registry.add(str(default["url"]), curated=True)
48
+ entries = catalog.rebuild_tap(tap)
49
+ except BoostError as e:
50
+ out.warn("could not tap %s: %s" % (default["name"], e.message))
51
+ rc = 1
52
+ continue
53
+ journal.log("tap", tap.name)
54
+ out.ok("tapped %s (%d skills) — %s"
55
+ % (tap.name, len(entries), default.get("focus", "")))
56
+ if args.spec:
57
+ tap = registry.add(args.spec, curated=args.curated)
58
+ entries = catalog.rebuild_tap(tap)
59
+ journal.log("tap", tap.name)
60
+ out.ok("Tapped %s (%d skills)" % (tap.name, len(entries)))
61
+ return rc
62
+
63
+
64
+ def cmd_untap(argv) -> int:
65
+ """boost untap NAME [--force]"""
66
+ p = argparse.ArgumentParser(
67
+ prog="boost untap",
68
+ description="Remove a registry tap")
69
+ p.add_argument("name", help="tap name (owner/repo or short alias)")
70
+ p.add_argument("-f", "--force", action="store_true",
71
+ help="skip the confirmation prompt")
72
+ p.add_argument("-y", "--yes", action="store_true", help=argparse.SUPPRESS)
73
+ args = p.parse_args(argv)
74
+
75
+ tap = registry.get(args.name)
76
+ dependent = sorted(n for n, e in lockfile.installed().items()
77
+ if e.get("tap") == tap.name)
78
+ if dependent:
79
+ out.warn("%d skill(s) installed from %s: %s"
80
+ % (len(dependent), tap.name, ", ".join(dependent)))
81
+ out.warn("installed skills keep working but lose their update source")
82
+ if not (args.force or args.yes) and not out.confirm(
83
+ "untap %s anyway?" % tap.name):
84
+ out.info("cancelled")
85
+ return 1
86
+ registry.remove(tap.name)
87
+ journal.log("untap", tap.name)
88
+ out.ok("untapped %s" % tap.name)
89
+ return 0
90
+
91
+
92
+ def _tap_updated(tap: "registry.Tap") -> str:
93
+ """Last-commit date of a tap clone, else the cache's generated age."""
94
+ if tap.is_cloned:
95
+ try:
96
+ # --date=short --format=%cd == %cs, but works on git < 2.21 too
97
+ proc = gitutil.run(["-C", str(tap.path), "log", "-1",
98
+ "--date=short", "--format=%cd"], check=False)
99
+ if proc.returncode == 0 and proc.stdout.strip():
100
+ return proc.stdout.strip()
101
+ except BoostError:
102
+ pass
103
+ try:
104
+ data = json.loads(tap.cache_file.read_text())
105
+ return util.rel_time(data.get("generated", ""))
106
+ except (OSError, ValueError):
107
+ return "?"
108
+
109
+
110
+ def cmd_taps(argv) -> int:
111
+ """boost taps [--json]"""
112
+ p = argparse.ArgumentParser(
113
+ prog="boost taps",
114
+ description="List all configured registry taps")
115
+ p.add_argument("--json", action="store_true",
116
+ help="machine-readable output")
117
+ args = p.parse_args(argv)
118
+
119
+ taps = []
120
+ total = 0
121
+ for tap in registry.list_taps():
122
+ skills = catalog.load_tap(tap)
123
+ total += len(skills)
124
+ taps.append({"name": tap.name, "url": tap.url, "curated": tap.curated,
125
+ "skills": len(skills), "updated": _tap_updated(tap)})
126
+ if args.json:
127
+ print(json.dumps(taps, indent=2))
128
+ return 0
129
+ if not taps:
130
+ out.info("no taps configured")
131
+ out.info(out.c("add the recommended registries with `boost tap --defaults`",
132
+ out.DIM))
133
+ return 0
134
+ rows = [(t["name"], str(t["skills"]), t["updated"],
135
+ "★" if t["curated"] else "", out.c(_tilde(t["url"]), out.DIM))
136
+ for t in taps]
137
+ out.table(rows, headers=("NAME", "SKILLS", "UPDATED", "", "URL"))
138
+ print()
139
+ out.dim("%d taps · %d skills" % (len(taps), total))
140
+ return 0
141
+
142
+
143
+ def cmd_outdated(argv) -> int:
144
+ """boost outdated [--json]"""
145
+ p = argparse.ArgumentParser(
146
+ prog="boost outdated",
147
+ description="Show skills with available updates")
148
+ p.add_argument("--json", action="store_true",
149
+ help="machine-readable output")
150
+ args = p.parse_args(argv)
151
+
152
+ results = []
153
+ heads = {} # tap name -> HEAD commit ("" when unknown)
154
+ for name, lk in sorted(lockfile.installed().items()):
155
+ tap_name = lk.get("tap", "local")
156
+ if tap_name == "local":
157
+ continue
158
+ matches = [e for e in catalog.find(name) if e["tap"] == tap_name]
159
+ if not matches:
160
+ continue
161
+ entry = matches[0]
162
+ latest = str(entry.get("version") or "0.0.0")
163
+ installed_v = str(lk.get("version") or "0.0.0")
164
+ stale, latest_disp = False, latest
165
+ if util.semver_gt(latest, installed_v):
166
+ stale = True
167
+ else:
168
+ if tap_name not in heads:
169
+ try:
170
+ tap = registry.get(tap_name)
171
+ heads[tap_name] = (gitutil.head_commit(tap.path)
172
+ if tap.is_cloned else "")
173
+ except BoostError:
174
+ heads[tap_name] = ""
175
+ head = heads[tap_name]
176
+ if head and head != lk.get("commit"):
177
+ try:
178
+ src = store.source_dir_for(entry)
179
+ except BoostError:
180
+ stale, latest_disp = True, "source missing"
181
+ else:
182
+ if util.sha256_dir(src) != lk.get("sha256"):
183
+ stale = True
184
+ latest_disp = "%s (%s)" % (latest, head[:7])
185
+ if stale:
186
+ results.append({"name": name, "installed": installed_v,
187
+ "latest": latest_disp, "tap": tap_name,
188
+ "pinned": bool(lk.get("pinned"))})
189
+
190
+ if args.json:
191
+ print(json.dumps(results, indent=2))
192
+ return 0
193
+ if not results:
194
+ out.ok("everything up to date")
195
+ return 0
196
+ rows = [(r["name"],
197
+ r["installed"] + (" (pinned)" if r["pinned"] else ""),
198
+ r["latest"], r["tap"]) for r in results]
199
+ out.table(rows, headers=("NAME", "INSTALLED", "LATEST", "TAP"))
200
+ print()
201
+ out.dim("%d outdated · `boost update` upgrades (pinned skills stay put)"
202
+ % len(results))
203
+ return 0