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