users.py
python
sha256:1ddad36d76d3a8d323f9b3664169cb184b7a38b39208214a2ae504154260826f
fix: show full cryptographic IDs in all human-readable CLI output
Sonnet 4.6
patch
35 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
35 days ago