gabriel / muse public
users.py python
410 lines 16.1 KB
Raw
sha256:1ddad36d76d3a8d323f9b3664169cb184b7a38b39208214a2ae504154260826f fix: show full cryptographic IDs in all human-readable CLI output Sonnet 4.6 patch 36 days ago
1 import argparse
2 from ._core import *
3
4 def run_user_read(args: argparse.Namespace) -> None:
5 """Fetch a user's public profile from MuseHub.
6
7 JSON output is the raw API response merged with the standard envelope.
8 Human-readable text goes to stderr.
9
10 Agent quickstart
11 ----------------
12 ::
13
14 muse hub user read gabriel --json
15 muse hub user read gabriel --json | jq '{username,repoCount,followerCount}'
16
17 JSON output keys (from hub): ``username``, ``displayName``, ``bio``,
18 ``avatarUrl``, ``repoCount``, ``followerCount``, ``followingCount``,
19 ``pinnedRepos``, ``createdAt``.
20
21 Exit codes
22 ----------
23 0 Success.
24 1 Auth error.
25 3 API error (404 if user not found).
26 """
27 elapsed = start_timer()
28 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
29
30 data = _hub_api(hub_url, identity, "GET", f"/api/users/{args.username}/profile")
31
32 if args.json_output:
33 print(json.dumps({**make_envelope(elapsed), **data}))
34 return
35
36 print(f"\n {data.get('username')} {data.get('displayName', '')}", file=sys.stderr)
37 if data.get("bio"):
38 print(f" {data['bio']}", file=sys.stderr)
39 print(f" Repos: {data.get('repoCount', 0)} Followers: {data.get('followerCount', 0)}", file=sys.stderr)
40
41 def run_user_update(args: argparse.Namespace) -> None:
42 """Update the authenticated user's profile on MuseHub.
43
44 At least one of ``--bio``, ``--avatar-url``, or ``--pinned-repo`` must be given.
45
46 JSON output is the raw API response merged with the standard envelope.
47 Human-readable text goes to stderr.
48
49 Agent quickstart
50 ----------------
51 ::
52
53 muse hub user update gabriel --bio 'Composer and engineer' --json
54 muse hub user update gabriel --avatar-url https://example.com/avatar.png --json
55 # → {"muse_version": "...", ..., "username": "gabriel", "bio": "..."}
56
57 JSON output keys (from hub): ``username``, ``displayName``, ``bio``,
58 ``avatarUrl``, ``pinnedRepos``.
59
60 Exit codes
61 ----------
62 0 Updated.
63 1 Auth error (caller must own the profile).
64 3 API error.
65 """
66 if args.bio is None and args.avatar_url is None and args.pinned_repo_ids is None:
67 print("❌ Provide at least one of --bio, --avatar-url, or --pinned-repo.", file=sys.stderr)
68 raise SystemExit(ExitCode.USER_ERROR)
69
70 elapsed = start_timer()
71 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
72
73 payload = {}
74 if args.bio is not None:
75 payload["bio"] = args.bio
76 if args.avatar_url is not None:
77 payload["avatarUrl"] = args.avatar_url
78 if args.pinned_repo_ids is not None:
79 payload["pinnedRepoIds"] = args.pinned_repo_ids
80
81 data = _hub_api(hub_url, identity, "PATCH", f"/api/users/{args.username}/profile", body=payload)
82
83 if args.json_output:
84 print(json.dumps({**make_envelope(elapsed), **data}))
85 return
86
87 print(f"✅ Profile for {args.username} updated.", file=sys.stderr)
88
89 def run_topic_list(args: argparse.Namespace) -> None:
90 """List topics on MuseHub, optionally filtered by query.
91
92 JSON output is the raw API response merged with the standard envelope.
93 Human-readable text goes to stderr.
94
95 Agent quickstart
96 ----------------
97 ::
98
99 muse hub topic list --json
100 muse hub topic list --query jazz --json | jq '.topics[].name'
101
102 JSON output keys (from hub): ``topics`` (list of ``{name, repoCount}``).
103
104 Exit codes
105 ----------
106 0 Success.
107 3 API error.
108 """
109 elapsed = start_timer()
110 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
111
112 params: dict[str, str | int] = {"limit": args.limit}
113 if args.query:
114 params["q"] = args.query
115
116 data = _hub_api(hub_url, identity, "GET", "/api/topics", params=params)
117
118 if args.json_output:
119 print(json.dumps({**make_envelope(elapsed), **data}))
120 return
121
122 topics = data.get("topics", [])
123 if not topics:
124 print(" No topics found.", file=sys.stderr)
125 return
126
127 for t in topics:
128 print(f" {t.get('name')} ({t.get('repoCount', 0)} repos)", file=sys.stderr)
129
130 def run_topic_set(args: argparse.Namespace) -> None:
131 """Set (replace) the topic list for a repo on MuseHub.
132
133 Pass no ``--topic`` flags to clear all topics.
134
135 JSON output is the raw API response merged with the standard envelope.
136 Human-readable text goes to stderr.
137
138 Agent quickstart
139 ----------------
140 ::
141
142 muse hub topic set --topic jazz --topic midi --topic generative --json
143 muse hub topic set --json # clears all topics
144 # → {"muse_version": "...", ..., "topics": ["jazz", "midi", "generative"]}
145
146 JSON output keys (from hub): ``topics`` (list of applied topic strings).
147
148 Exit codes
149 ----------
150 0 Updated.
151 1 Auth error.
152 2 Not inside a Muse repository.
153 3 API error.
154 """
155 elapsed = start_timer()
156 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
157 repo_id = _resolve_repo_id(hub_url, identity)
158
159 payload = {"topics": args.topics}
160 data = _hub_api(hub_url, identity, "PUT", f"/api/repos/{repo_id}/topics", body=payload)
161
162 if args.json_output:
163 print(json.dumps({**make_envelope(elapsed), **data}))
164 return
165
166 applied: list[str] = data.get("topics", [])
167 if applied:
168 print(f"✅ Topics set: {', '.join(applied)}", file=sys.stderr)
169 else:
170 print("✅ All topics cleared.", file=sys.stderr)
171
172 def run_fork_create(args: argparse.Namespace) -> None:
173 """Fork a MuseHub repository into your account.
174
175 ``--repo`` must be in ``owner/repo`` format.
176
177 JSON output is the raw API response merged with the standard envelope.
178 Human-readable text goes to stderr.
179
180 Agent quickstart
181 ----------------
182 ::
183
184 muse hub fork create --repo gabriel/muse --json
185 muse hub fork create --repo gabriel/muse --slug my-muse-fork --json
186 # → {"muse_version": "...", ..., "owner": "you", "slug": "muse", "forkedFrom": "..."}
187
188 JSON output keys (from hub): ``repoId``, ``owner``, ``slug``,
189 ``forkedFrom``, ``createdAt``.
190
191 Exit codes
192 ----------
193 0 Forked.
194 1 Auth error.
195 3 API error (404 if source repo not found).
196 """
197 elapsed = start_timer()
198 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
199 repo_id = _resolve_repo_id_from_owner_repo(hub_url, identity, args.repo)
200
201 payload = {}
202 if args.slug:
203 payload["slug"] = args.slug
204
205 data = _hub_api(hub_url, identity, "POST", f"/api/repos/{repo_id}/forks", body=payload)
206
207 if args.json_output:
208 print(json.dumps({**make_envelope(elapsed), **data}))
209 return
210
211 print(f"✅ Forked to {data.get('owner')}/{data.get('slug')} (id={str(data.get('repoId',''))}).", file=sys.stderr)
212
213 def run_fork_list(args: argparse.Namespace) -> None:
214 """List all direct forks of a MuseHub repository.
215
216 ``--repo`` must be in ``owner/repo`` format.
217
218 JSON output is the raw API response merged with the standard envelope.
219 Human-readable text goes to stderr.
220
221 Agent quickstart
222 ----------------
223 ::
224
225 muse hub fork list --repo gabriel/muse --json
226 muse hub fork list --repo gabriel/muse --json | jq '.forks[] | .owner + "/" + .slug'
227
228 JSON output keys (from hub): ``forks`` (list), ``total``.
229 Each fork: ``repoId``, ``owner``, ``slug``, ``createdAt``.
230
231 Exit codes
232 ----------
233 0 Success.
234 3 API error.
235 """
236 elapsed = start_timer()
237 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
238 repo_id = _resolve_repo_id_from_owner_repo(hub_url, identity, args.repo)
239
240 data = _hub_api(hub_url, identity, "GET", f"/api/repos/{repo_id}/forks")
241
242 if args.json_output:
243 print(json.dumps({**make_envelope(elapsed), **data}))
244 return
245
246 forks = data.get("forks", [])
247 if not forks:
248 print(" No forks found.", file=sys.stderr)
249 return
250
251 print(f"\n Forks ({len(forks)} total)", file=sys.stderr)
252 for f in forks:
253 print(f" {f.get('owner')}/{f.get('slug')}", file=sys.stderr)
254
255 def register(subs: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
256 """Register users subcommands."""
257 # ── user ──────────────────────────────────────────────────────────────────
258 user_p = subs.add_parser(
259 "user",
260 help="Manage user profiles on MuseHub.",
261 formatter_class=argparse.RawDescriptionHelpFormatter,
262 )
263 user_subs = user_p.add_subparsers(dest="user_subcommand", metavar="USER_COMMAND")
264 user_subs.required = True
265
266 def _user_hub_arg(p: argparse.ArgumentParser) -> None:
267 p.add_argument("--hub", dest="hub", default=None, metavar="URL",
268 help="Override the hub URL from config.")
269 p.add_argument("--json", "-j", action="store_true", dest="json_output",
270 help="Emit JSON output.")
271
272 user_read_p = user_subs.add_parser(
273 "read", help="Read a user's public profile.",
274 description=(
275 "Fetch a user's public profile: bio, avatar, pinned repos, stats.\n\n"
276 " muse hub user read gabriel\n"
277 " muse hub user read gabriel --json\n\n"
278 "Exit codes: 0 success, 1 auth error, 3 API error (404 if not found)."
279 ),
280 formatter_class=argparse.RawDescriptionHelpFormatter,
281 )
282 _user_hub_arg(user_read_p)
283 user_read_p.add_argument("username", help="MuseHub username.")
284 user_read_p.set_defaults(func=run_user_read)
285
286 user_update_p = user_subs.add_parser(
287 "update", help="Update your own profile.",
288 description=(
289 "Update the authenticated user's profile bio, avatar URL, or pinned repos.\n\n"
290 " muse hub user update --bio 'Composer and engineer'\n"
291 " muse hub user update --avatar-url https://example.com/avatar.png --json\n\n"
292 "Exit codes: 0 success, 1 auth error, 3 API error."
293 ),
294 formatter_class=argparse.RawDescriptionHelpFormatter,
295 )
296 _user_hub_arg(user_update_p)
297 user_update_p.add_argument("username", help="Your MuseHub username.")
298 user_update_p.add_argument("--bio", default=None, help="Profile bio text.")
299 user_update_p.add_argument("--avatar-url", dest="avatar_url", default=None,
300 help="Avatar image URL.")
301 user_update_p.add_argument("--pinned-repo", dest="pinned_repo_ids", action="append",
302 default=None, metavar="REPO_ID",
303 help="Pinned repo ID (repeatable, replaces all pinned repos).")
304 user_update_p.set_defaults(func=run_user_update)
305
306 user_p.set_defaults(func=lambda a: user_p.print_help())
307
308 # ── topic ─────────────────────────────────────────────────────────────────
309 topic_p = subs.add_parser(
310 "topic",
311 help="Manage repo topics on MuseHub.",
312 formatter_class=argparse.RawDescriptionHelpFormatter,
313 )
314 topic_subs = topic_p.add_subparsers(dest="topic_subcommand", metavar="TOPIC_COMMAND")
315 topic_subs.required = True
316
317 def _topic_repo_args(p: argparse.ArgumentParser) -> None:
318 p.add_argument("--hub", dest="hub", default=None, metavar="URL",
319 help="Override the hub URL from config.")
320 p.add_argument("--repo", dest="repo", default=None, metavar="OWNER/REPO",
321 help="Specify repo as owner/repo.")
322 p.add_argument("--json", "-j", action="store_true", dest="json_output",
323 help="Emit JSON output.")
324
325 topic_list_p = topic_subs.add_parser(
326 "list", help="Browse global topics.",
327 description=(
328 "List all topics registered on MuseHub, optionally filtered by query.\n\n"
329 " muse hub topic list\n"
330 " muse hub topic list --query jazz --json\n\n"
331 "Exit codes: 0 success, 3 API error."
332 ),
333 formatter_class=argparse.RawDescriptionHelpFormatter,
334 )
335 topic_list_p.add_argument("--hub", dest="hub", default=None, metavar="URL",
336 help="Override the hub URL from config.")
337 topic_list_p.add_argument("--query", default=None, help="Filter topics by substring.")
338 topic_list_p.add_argument("--limit", type=int, default=50, metavar="N",
339 help="Max topics to return (default 50).")
340 topic_list_p.add_argument("--json", "-j", action="store_true", dest="json_output",
341 help="Emit JSON output.")
342 topic_list_p.set_defaults(func=run_topic_list)
343
344 topic_set_p = topic_subs.add_parser(
345 "set", help="Set topics for a repo.",
346 description=(
347 "Replace the full topic list for a repo (max 20 topics).\n\n"
348 " muse hub topic set --topic jazz --topic midi --topic generative\n"
349 " muse hub topic set --topic jazz --json\n\n"
350 "Pass no --topic flags to clear all topics.\n\n"
351 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
352 ),
353 formatter_class=argparse.RawDescriptionHelpFormatter,
354 )
355 _topic_repo_args(topic_set_p)
356 topic_set_p.add_argument("--topic", dest="topics", action="append", default=[],
357 metavar="TOPIC", help="Topic to set (repeatable).")
358 topic_set_p.set_defaults(func=run_topic_set)
359
360 topic_p.set_defaults(func=lambda a: topic_p.print_help())
361
362 # ── fork ──────────────────────────────────────────────────────────────────
363 fork_p = subs.add_parser(
364 "fork",
365 help="Fork a repo on MuseHub.",
366 formatter_class=argparse.RawDescriptionHelpFormatter,
367 )
368 fork_subs = fork_p.add_subparsers(dest="fork_subcommand", metavar="FORK_COMMAND")
369 fork_subs.required = True
370
371 fork_create_p = fork_subs.add_parser(
372 "create", help="Fork a repository.",
373 description=(
374 "Fork a MuseHub repository into your own account.\n\n"
375 " muse hub fork create --repo gabriel/muse\n"
376 " muse hub fork create --repo gabriel/muse --slug my-muse-fork --json\n\n"
377 "Exit codes: 0 success, 1 auth error, 3 API error."
378 ),
379 formatter_class=argparse.RawDescriptionHelpFormatter,
380 )
381 fork_create_p.add_argument("--hub", dest="hub", default=None, metavar="URL",
382 help="Override the hub URL from config.")
383 fork_create_p.add_argument("--repo", dest="repo", required=True, metavar="OWNER/REPO",
384 help="Source repo to fork, as owner/repo.")
385 fork_create_p.add_argument("--slug", default=None,
386 help="Custom slug for the fork (defaults to source slug).")
387 fork_create_p.add_argument("--json", "-j", action="store_true", dest="json_output",
388 help="Emit JSON output.")
389 fork_create_p.set_defaults(func=run_fork_create)
390
391 fork_list_p = fork_subs.add_parser(
392 "list", help="List forks of a repo.",
393 description=(
394 "List all direct forks of a repository.\n\n"
395 " muse hub fork list --repo gabriel/muse\n"
396 " muse hub fork list --repo gabriel/muse --json\n\n"
397 "Exit codes: 0 success, 3 API error."
398 ),
399 formatter_class=argparse.RawDescriptionHelpFormatter,
400 )
401 fork_list_p.add_argument("--hub", dest="hub", default=None, metavar="URL",
402 help="Override the hub URL from config.")
403 fork_list_p.add_argument("--repo", dest="repo", required=True, metavar="OWNER/REPO",
404 help="Source repo, as owner/repo.")
405 fork_list_p.add_argument("--json", "-j", action="store_true", dest="json_output",
406 help="Emit JSON output.")
407 fork_list_p.set_defaults(func=run_fork_list)
408
409 fork_p.set_defaults(func=lambda a: fork_p.print_help())
410
File History 1 commit
sha256:1ddad36d76d3a8d323f9b3664169cb184b7a38b39208214a2ae504154260826f fix: show full cryptographic IDs in all human-readable CLI output Sonnet 4.6 patch 36 days ago