gabriel / muse public
releases.py python
472 lines 18.3 KB
Raw
sha256:1ddad36d76d3a8d323f9b3664169cb184b7a38b39208214a2ae504154260826f fix: show full cryptographic IDs in all human-readable CLI output Sonnet 4.6 patch 8 days ago
1 import argparse
2 from ._core import *
3
4 def run_release_list(args: argparse.Namespace) -> None:
5 """List releases for the repo on MuseHub, newest first.
6
7 Supports cursor pagination.
8
9 JSON output is the raw API response merged with the standard envelope.
10 Human-readable text goes to stderr.
11
12 Agent quickstart
13 ----------------
14 ::
15
16 muse hub release list --json
17 muse hub release list --limit 10 --json | jq '.releases[].tag'
18 muse hub release list --cursor <token> --json
19
20 JSON output keys (from hub): ``releases`` (list), ``nextCursor``
21 (str|null), ``total`` (int). Each release: ``tag``, ``title``,
22 ``channel``, ``isDraft``, ``createdAt``, ``assetCount``.
23
24 Exit codes
25 ----------
26 0 Success (including empty list).
27 1 Validation or auth error.
28 2 Not inside a Muse repository.
29 3 API error.
30 """
31 json_output: bool = args.json_output
32 elapsed = start_timer()
33 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
34 repo_id = _resolve_repo_id(hub_url, identity)
35
36 params: dict[str, str | int] = {"limit": args.limit}
37 if args.cursor:
38 params["cursor"] = args.cursor
39
40 data = _hub_api(hub_url, identity, "GET", f"/api/repos/{repo_id}/releases", params=params)
41
42 if json_output:
43 print(json.dumps({**make_envelope(elapsed), **data}))
44 return
45
46 releases = data.get("releases", [])
47 if not releases:
48 print(" No releases found.", file=sys.stderr)
49 return
50
51 print(f"\n Releases ({len(releases)} shown)", file=sys.stderr)
52 print(f" {'─' * 50}", file=sys.stderr)
53 for r in releases:
54 draft = " [draft]" if r.get("isDraft") else ""
55 channel = r.get("channel", "stable")
56 print(f" {r.get('tag')} {r.get('title', '')} ({channel}){draft}", file=sys.stderr)
57 next_c = data.get("nextCursor")
58 if next_c:
59 print(f"\n (more — pass --cursor {next_c})", file=sys.stderr)
60
61 def run_release_create(args: argparse.Namespace) -> None:
62 """Create a new release for the repo on MuseHub.
63
64 Tag must be a valid semver string (e.g. ``v1.0.0``, ``v2.0.0-beta.1``).
65
66 JSON output is the raw API response merged with the standard envelope.
67 Human-readable text goes to stderr.
68
69 Agent quickstart
70 ----------------
71 ::
72
73 muse hub release create --tag v1.0.0 --title 'First release' --json
74 muse hub release create --tag v1.0.0 --body 'Notes' --channel beta --json
75 # → {"muse_version": "...", ..., "releaseId": "...", "tag": "v1.0.0"}
76
77 JSON output keys (from hub): ``releaseId``, ``tag``, ``title``,
78 ``channel``, ``isDraft``, ``createdAt``.
79
80 Exit codes
81 ----------
82 0 Release created.
83 1 Validation or auth error.
84 2 Not inside a Muse repository.
85 3 API error (409 if tag already exists).
86 """
87 if not args.tag.strip():
88 print("❌ --tag must not be empty.", file=sys.stderr)
89 raise SystemExit(ExitCode.USER_ERROR)
90
91 elapsed = start_timer()
92 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
93 repo_id = _resolve_repo_id(hub_url, identity)
94
95 payload = {
96 "tag": args.tag,
97 "title": args.title,
98 "body": _resolve_body(args),
99 "channel": args.channel,
100 "isDraft": args.is_draft,
101 }
102 if args.commit_id:
103 payload["commitId"] = args.commit_id
104
105 data = _hub_api(hub_url, identity, "POST", f"/api/repos/{repo_id}/releases", body=payload)
106
107 if args.json_output:
108 print(json.dumps({**make_envelope(elapsed), **data}))
109 return
110
111 print(f"✅ Release {data.get('tag')} created (id={str(data.get('releaseId',''))}).", file=sys.stderr)
112
113 def run_release_read(args: argparse.Namespace) -> None:
114 """Fetch a single release by tag from MuseHub.
115
116 JSON output is the raw API response merged with the standard envelope.
117 Human-readable text goes to stderr.
118
119 Agent quickstart
120 ----------------
121 ::
122
123 muse hub release read v1.0.0 --json
124 muse hub release read v1.0.0 --json | jq '{tag,title,channel,isDraft}'
125
126 JSON output keys (from hub): ``releaseId``, ``tag``, ``title``, ``body``,
127 ``channel``, ``isDraft``, ``commitId``, ``assetCount``, ``createdAt``,
128 ``downloadStats``.
129
130 Exit codes
131 ----------
132 0 Success.
133 1 Auth error.
134 2 Not inside a Muse repository.
135 3 API error (404 if tag not found).
136 """
137 elapsed = start_timer()
138 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
139 repo_id = _resolve_repo_id(hub_url, identity)
140
141 data = _hub_api(hub_url, identity, "GET", f"/api/repos/{repo_id}/releases/{args.tag}")
142
143 if args.json_output:
144 print(json.dumps({**make_envelope(elapsed), **data}))
145 return
146
147 print(f"\n Release: {data.get('tag')} {data.get('title', '')}", file=sys.stderr)
148 print(f" Channel: {data.get('channel', 'stable')} Draft: {data.get('isDraft', False)}", file=sys.stderr)
149 if data.get("body"):
150 print(f"\n {data['body'][:200]}", file=sys.stderr)
151
152 def run_release_delete(args: argparse.Namespace) -> None:
153 """Delete a release and its assets from MuseHub.
154
155 JSON output includes envelope fields plus ``deleted`` (bool) and
156 ``tag`` (str).
157
158 Agent quickstart
159 ----------------
160 ::
161
162 muse hub release delete v1.0.0 --json
163 # → {"muse_version": "...", ..., "deleted": true, "tag": "v1.0.0"}
164
165 Exit codes
166 ----------
167 0 Deleted.
168 1 Auth error.
169 2 Not inside a Muse repository.
170 3 API error (404 if tag not found).
171 """
172 elapsed = start_timer()
173 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
174 repo_id = _resolve_repo_id(hub_url, identity)
175
176 _hub_api(hub_url, identity, "DELETE", f"/api/repos/{repo_id}/releases/{args.tag}")
177
178 if args.json_output:
179 print(json.dumps({**make_envelope(elapsed), **{"deleted": True, "tag": args.tag}}))
180 return
181
182 print(f"✅ Release {args.tag} deleted.", file=sys.stderr)
183
184 def run_release_asset_list(args: argparse.Namespace) -> None:
185 """List assets attached to a release.
186
187 JSON output is the raw API response merged with the standard envelope.
188 Human-readable text goes to stderr.
189
190 Agent quickstart
191 ----------------
192 ::
193
194 muse hub release asset-list v1.0.0 --json
195 muse hub release asset-list v1.0.0 --json | jq '.assets[].downloadUrl'
196
197 JSON output keys (from hub): ``assets`` (list), ``nextCursor``, ``total``.
198 Each asset: ``assetId``, ``name``, ``label``, ``contentType``, ``size``,
199 ``downloadUrl``, ``downloadCount``.
200
201 Exit codes
202 ----------
203 0 Success.
204 1 Auth error.
205 2 Not inside a Muse repository.
206 3 API error.
207 """
208 elapsed = start_timer()
209 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
210 repo_id = _resolve_repo_id(hub_url, identity)
211
212 data = _hub_api(hub_url, identity, "GET", f"/api/repos/{repo_id}/releases/{args.tag}/assets")
213
214 if args.json_output:
215 print(json.dumps({**make_envelope(elapsed), **data}))
216 return
217
218 assets = data.get("assets", [])
219 if not assets:
220 print(f" No assets for release {args.tag}.", file=sys.stderr)
221 return
222
223 print(f"\n Assets for {args.tag} ({len(assets)} total)", file=sys.stderr)
224 print(f" {'─' * 50}", file=sys.stderr)
225 for a in assets:
226 size_kb = int(a.get("size", 0)) // 1024
227 dl = a.get("downloadCount", 0)
228 print(f" {a.get('name')} {size_kb}KB {dl} downloads {a.get('downloadUrl', '')[:60]}", file=sys.stderr)
229
230 def run_release_asset_attach(args: argparse.Namespace) -> None:
231 """Attach a downloadable asset to a release on MuseHub.
232
233 JSON output is the raw API response merged with the standard envelope.
234 Human-readable text goes to stderr.
235
236 Agent quickstart
237 ----------------
238 ::
239
240 muse hub release asset-attach v1.0.0 --name track.mid --url https://cdn.example.com/track.mid --json
241 muse hub release asset-attach v1.0.0 --name mpack.zip --url <url> --content-type application/zip --json
242 # → {"muse_version": "...", ..., "assetId": "...", "name": "track.mid"}
243
244 JSON output keys (from hub): ``assetId``, ``name``, ``label``,
245 ``contentType``, ``size``, ``downloadUrl``.
246
247 Exit codes
248 ----------
249 0 Asset attached.
250 1 Auth error.
251 2 Not inside a Muse repository.
252 3 API error.
253 """
254 if not args.name.strip():
255 print("❌ --name must not be empty.", file=sys.stderr)
256 raise SystemExit(ExitCode.USER_ERROR)
257 if not args.download_url.strip():
258 print("❌ --url must not be empty.", file=sys.stderr)
259 raise SystemExit(ExitCode.USER_ERROR)
260
261 elapsed = start_timer()
262 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
263 repo_id = _resolve_repo_id(hub_url, identity)
264
265 payload = {
266 "name": args.name,
267 "label": args.label,
268 "contentType": args.content_type,
269 "size": args.size,
270 "downloadUrl": args.download_url,
271 }
272
273 data = _hub_api(hub_url, identity, "POST",
274 f"/api/repos/{repo_id}/releases/{args.tag}/assets", body=payload)
275
276 if args.json_output:
277 print(json.dumps({**make_envelope(elapsed), **data}))
278 return
279
280 print(f"✅ Asset '{data.get('name')}' attached to release {args.tag}.", file=sys.stderr)
281
282 def run_release_asset_delete(args: argparse.Namespace) -> None:
283 """Remove an asset from a release on MuseHub.
284
285 JSON output includes envelope fields plus ``deleted`` (bool) and
286 ``assetId`` (str).
287
288 Agent quickstart
289 ----------------
290 ::
291
292 muse hub release asset-delete v1.0.0 --asset-id <id> --json
293 # → {"muse_version": "...", ..., "deleted": true, "assetId": "..."}
294
295 Exit codes
296 ----------
297 0 Deleted.
298 1 Auth error.
299 2 Not inside a Muse repository.
300 3 API error (404 if asset not found).
301 """
302 if not args.asset_id.strip():
303 print("❌ --asset-id must not be empty.", file=sys.stderr)
304 raise SystemExit(ExitCode.USER_ERROR)
305
306 elapsed = start_timer()
307 hub_url, identity = _get_hub_and_identity(hub_url_override=_resolve_hub_override(args))
308 repo_id = _resolve_repo_id(hub_url, identity)
309
310 _hub_api(hub_url, identity, "DELETE",
311 f"/api/repos/{repo_id}/releases/{args.tag}/assets/{args.asset_id}")
312
313 if args.json_output:
314 print(json.dumps({**make_envelope(elapsed), **{"deleted": True, "assetId": args.asset_id}}))
315 return
316
317 print(f"✅ Asset {args.asset_id} deleted from release {args.tag}.", file=sys.stderr)
318
319 def register(subs: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
320 """Register releases subcommands."""
321 # ── release ───────────────────────────────────────────────────────────────
322 release_p = subs.add_parser(
323 "release",
324 help="Manage releases on MuseHub.",
325 formatter_class=argparse.RawDescriptionHelpFormatter,
326 )
327 release_subs = release_p.add_subparsers(dest="release_subcommand", metavar="RELEASE_COMMAND")
328 release_subs.required = True
329
330 def _release_repo_args(p: argparse.ArgumentParser) -> None:
331 p.add_argument("--hub", dest="hub", default=None, metavar="URL",
332 help="Override the hub URL from config.")
333 p.add_argument("--repo", dest="repo", default=None, metavar="OWNER/REPO",
334 help="Specify repo as owner/repo.")
335 p.add_argument("--json", "-j", action="store_true", dest="json_output",
336 help="Emit JSON output.")
337
338 # release list
339 release_list_p = release_subs.add_parser(
340 "list", help="List releases.",
341 description=(
342 "List all releases for the repo, newest first.\n\n"
343 " muse hub release list\n"
344 " muse hub release list --limit 10 --json\n\n"
345 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
346 ),
347 formatter_class=argparse.RawDescriptionHelpFormatter,
348 )
349 _release_repo_args(release_list_p)
350 release_list_p.add_argument("--limit", type=int, default=20, metavar="N",
351 help="Max releases to return (default 20).")
352 release_list_p.add_argument("--cursor", default=None, metavar="CURSOR",
353 help="Pagination cursor.")
354 release_list_p.set_defaults(func=run_release_list)
355
356 # release create
357 release_create_p = release_subs.add_parser(
358 "create", help="Create a new release.",
359 description=(
360 "Create a new release for the repo.\n\n"
361 " muse hub release create --tag v1.0.0 --title 'First release'\n"
362 " muse hub release create --tag v1.0.0 --body 'Notes' --channel stable --json\n\n"
363 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
364 ),
365 formatter_class=argparse.RawDescriptionHelpFormatter,
366 )
367 _release_repo_args(release_create_p)
368 release_create_p.add_argument("--tag", required=True, help="Semver tag, e.g. v1.0.0.")
369 release_create_p.add_argument("--title", default="", help="Release title.")
370 release_create_p.add_argument("--body", default="", help="Release notes (Markdown, inline text).")
371 release_create_p.add_argument(
372 "--body-file", default=None, metavar="PATH", dest="body_file",
373 help="Read release notes from PATH. Pass '-' for stdin. Takes precedence over --body.",
374 )
375 release_create_p.add_argument("--commit-id", dest="commit_id", default=None,
376 help="Pin release to a specific commit ID.")
377 release_create_p.add_argument(
378 "--channel", default="stable",
379 choices=["stable", "beta", "alpha", "nightly"],
380 help="Distribution channel (default: stable).",
381 )
382 release_create_p.add_argument("--draft", action="store_true", dest="is_draft",
383 help="Save as draft — not yet publicly visible.")
384 release_create_p.set_defaults(func=run_release_create)
385
386 # release read
387 release_read_p = release_subs.add_parser(
388 "read", help="Read a release by tag.",
389 description=(
390 "Fetch a single release by its tag.\n\n"
391 " muse hub release read v1.0.0\n"
392 " muse hub release read v1.0.0 --json\n\n"
393 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
394 ),
395 formatter_class=argparse.RawDescriptionHelpFormatter,
396 )
397 _release_repo_args(release_read_p)
398 release_read_p.add_argument("tag", help="Release tag, e.g. v1.0.0.")
399 release_read_p.set_defaults(func=run_release_read)
400
401 # release delete
402 release_delete_p = release_subs.add_parser(
403 "delete", help="Delete a release by tag.",
404 description=(
405 "Delete a release. Assets attached to the release are also removed.\n\n"
406 " muse hub release delete v1.0.0\n"
407 " muse hub release delete v1.0.0 --json\n\n"
408 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
409 ),
410 formatter_class=argparse.RawDescriptionHelpFormatter,
411 )
412 _release_repo_args(release_delete_p)
413 release_delete_p.add_argument("tag", help="Release tag to delete.")
414 release_delete_p.set_defaults(func=run_release_delete)
415
416 # release asset-list
417 release_asset_list_p = release_subs.add_parser(
418 "asset-list", help="List assets attached to a release.",
419 description=(
420 "List all assets for a release.\n\n"
421 " muse hub release asset-list v1.0.0\n"
422 " muse hub release asset-list v1.0.0 --json\n\n"
423 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
424 ),
425 formatter_class=argparse.RawDescriptionHelpFormatter,
426 )
427 _release_repo_args(release_asset_list_p)
428 release_asset_list_p.add_argument("tag", help="Release tag.")
429 release_asset_list_p.set_defaults(func=run_release_asset_list)
430
431 # release asset-attach
432 release_asset_attach_p = release_subs.add_parser(
433 "asset-attach", help="Attach an asset to a release.",
434 description=(
435 "Attach a downloadable asset to a release.\n\n"
436 " muse hub release asset-attach v1.0.0 --name track.mid --url https://cdn.example.com/track.mid\n"
437 " muse hub release asset-attach v1.0.0 --name mpack.zip --url <url> --content-type application/zip --json\n\n"
438 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
439 ),
440 formatter_class=argparse.RawDescriptionHelpFormatter,
441 )
442 _release_repo_args(release_asset_attach_p)
443 release_asset_attach_p.add_argument("tag", help="Release tag.")
444 release_asset_attach_p.add_argument("--name", required=True, help="Filename shown in the UI.")
445 release_asset_attach_p.add_argument("--url", required=True, dest="download_url",
446 help="Direct download URL for the asset.")
447 release_asset_attach_p.add_argument("--label", default="", help="Human-readable label.")
448 release_asset_attach_p.add_argument("--content-type", dest="content_type", default="",
449 help="MIME type, e.g. audio/midi.")
450 release_asset_attach_p.add_argument("--size", type=int, default=0,
451 help="File size in bytes (0 if unknown).")
452 release_asset_attach_p.set_defaults(func=run_release_asset_attach)
453
454 # release asset-delete
455 release_asset_delete_p = release_subs.add_parser(
456 "asset-delete", help="Delete an asset from a release.",
457 description=(
458 "Remove an asset from a release by asset ID.\n\n"
459 " muse hub release asset-delete v1.0.0 --asset-id <id>\n"
460 " muse hub release asset-delete v1.0.0 --asset-id <id> --json\n\n"
461 "Exit codes: 0 success, 1 auth error, 2 not in repo, 3 API error."
462 ),
463 formatter_class=argparse.RawDescriptionHelpFormatter,
464 )
465 _release_repo_args(release_asset_delete_p)
466 release_asset_delete_p.add_argument("tag", help="Release tag.")
467 release_asset_delete_p.add_argument("--asset-id", dest="asset_id", required=True,
468 help="ID of the asset to delete.")
469 release_asset_delete_p.set_defaults(func=run_release_asset_delete)
470
471 release_p.set_defaults(func=lambda a: release_p.print_help())
472
File History 1 commit
sha256:1ddad36d76d3a8d323f9b3664169cb184b7a38b39208214a2ae504154260826f fix: show full cryptographic IDs in all human-readable CLI output Sonnet 4.6 patch 8 days ago