gabriel / muse public
app.py python
681 lines 26.8 KB
Raw
sha256:24d9cb81ac1dd071e5d98862c740ad101180588361b4f96a790af79a99aed901 docs: architectural plan for unified-store snapshot-content… Sonnet 5 10 days ago
1 """Muse CLI — entry point for the ``muse`` console script.
2
3 Two-tier command architecture
4 -------------------------------
5
6 **Tier 1 — Core + Low-level** (top-level ``muse …``)
7 All VCS commands live here — both human-facing and the machine-readable,
8 JSON-outputting primitives that scripts and agents use. Agents reach
9 every command in one token.
10
11 **Tier 2 — Semantic** (``muse midi …``, ``muse code …``, ``muse coord …``)
12 Domain-specific commands that interpret multidimensional state. Each
13 sub-namespace is served by the corresponding ``muse/plugins/`` plugin.
14
15 Top-level commands (alphabetical)::
16
17 agent annotate archive attributes auth
18 bisect blame branch bundle
19 cat cat-object check check-attr
20 check-ignore check-ref-format checkout cherry-pick
21 clean clone code commit
22 commit-graph commit-tree config conflicts
23 content-grep coord describe diff
24 domain domain-info domains fetch for-each-ref
25 gc hash-object hub init
26 log ls-files ls-remote merge
27 merge-base mist midi name-rev pack-objects
28 migrate path pull push rebase read-commit
29 read-snapshot reflog release remote
30 reset resolve revert rev-parse
31 rm
32 shortlog read show-ref sign
33 snapshot
34 snapshot-diff shelf status symbolic-ref
35 tag unpack-objects update-ref verify
36 verify-object verify-pack workspace
37 worktree
38
39 Identity & hub fabric::
40
41 auth keygen muse auth keygen [--hub HUB]
42 auth register muse auth register [--hub HUB] [--handle NAME] [--agent]
43 auth whoami muse auth whoami [--json]
44 auth logout muse auth logout [--hub HUB]
45 hub connect muse hub connect <url>
46 hub status muse hub status [--json]
47 hub disconnect muse hub disconnect
48 hub ping muse hub ping
49 config show muse config show [--json]
50 config get muse config get <key>
51 config set muse config set <key> <value>
52 config edit muse config edit
53
54 MIDI domain commands (``muse midi …``, alphabetical) — disabled pending rollout::
55
56 agent-map arpeggiate cadence check
57 compare contour density find-phrase
58 harmony humanize instrumentation invert
59 mix motif normalize note-blame
60 note-hotspots note-log notes piano-roll
61 quantize query retrograde rhythm
62 scale shard tempo tension
63 transpose velocity-profile voice-leading
64
65 Code domain commands (``muse code …``, alphabetical)::
66
67 add age api-surface blast-risk
68 blame breakage cat checkout-symbol
69 clones code-check code-query codemap
70 compare contract coupling coverage
71 dead deps detect-refactor docs
72 entangle find-symbol gravity grep
73 hotspots impact index invariants
74 languages lineage narrative patch
75 predict query query-history rename
76 reset semantic-cherry-pick semantic-test-coverage stable
77 symbol-log symbols test type
78 velocity
79
80 Coordination commands (``muse coord …``, alphabetical)::
81
82 cancel-task claim complete dag
83 enqueue fail-task forecast gc
84 heartbeat intent list plan-merge
85 reconcile release reserve shard
86 sync tasks watch
87 """
88
89 import argparse
90 import difflib
91 import os
92 import signal
93 import sys
94
95 from muse.cli.domain_command_registry import register_namespace
96
97 # Restore the default SIGPIPE handler so that piping muse output to commands
98 # that read partial output (head, grep, jq, etc.) exits cleanly instead of
99 # raising BrokenPipeError and printing a Python traceback. This matters most
100 # for agents that routinely pipe muse JSON output to filters.
101 if hasattr(signal, "SIGPIPE"):
102 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
103
104 from muse.cli.commands import (
105 age as age_cmd,
106 agent as agent_cmd,
107 agent_config,
108 # agent_map, # midi — disabled until midi rollout
109 annotate,
110 api_surface,
111 apply,
112 apply_patch,
113 archive,
114 # arpeggiate, # midi — disabled until midi rollout
115 attributes,
116 auth,
117 trust,
118 bisect,
119 blame,
120 blast_risk,
121 branch,
122 breakage,
123 bridge,
124 bundle,
125 # cadence, # midi — disabled until midi rollout
126 cat,
127 core_cat,
128 check,
129 checkout,
130 checkout_symbol,
131 cherry_pick,
132 clean,
133 clone,
134 clones,
135 code_check,
136 code_query,
137 code_stage,
138 codemap,
139 commit,
140 compare,
141 config_cmd,
142 conflicts,
143 content_grep,
144 contract,
145 # contour, # midi — disabled until midi rollout
146 coord_gc,
147 coord_sync,
148 core_blame,
149 coupling,
150 coverage,
151 dag as dag_cmd,
152 dead,
153 # density, # midi — disabled until midi rollout
154 deps,
155 describe,
156 detect_refactor,
157 diff,
158 docs_cmd,
159 domain_cmd,
160 domains,
161 entangle,
162 fetch,
163 # find_phrase, # midi — disabled until midi rollout
164 find_symbol,
165 forecast,
166 gc,
167 gravity,
168 grep,
169 harmony,
170 # midi_harmony, # midi — disabled until midi rollout
171 heartbeat_coord,
172 hotspots,
173 hub,
174 # humanize, # midi — disabled until midi rollout
175 impact,
176 index_rebuild,
177 init,
178 # instrumentation, # midi — disabled until midi rollout
179 intent,
180 invariants,
181 # invert, # midi — disabled until midi rollout
182 languages,
183 lineage,
184 list_coord,
185 log,
186 maintenance,
187 merge,
188 merge_base,
189 migrate,
190 migrate_cmd,
191 mist as mist_cmd,
192 mv,
193 # midi_check, # midi — disabled until midi rollout
194 # midi_compare, # midi — disabled until midi rollout
195 # midi_query, # midi — disabled until midi rollout
196 # midi_shard, # midi — disabled until midi rollout
197 # mix, # midi — disabled until midi rollout
198 # motif_detect, # midi — disabled until midi rollout
199 narrative,
200 # note_blame, # midi — disabled until midi rollout
201 # note_hotspots, # midi — disabled until midi rollout
202 # note_log, # midi — disabled until midi rollout
203 # notes, # midi — disabled until midi rollout
204 patch,
205 path_cmd,
206 # piano_roll, # midi — disabled until midi rollout
207 plan_merge,
208 predict,
209 prune,
210 pull,
211 push,
212 # quantize, # midi — disabled until midi rollout
213 query,
214 query_history,
215 range_diff,
216 rebase,
217 reconcile,
218 reflog,
219 release as release_cmd,
220 release_coord,
221 remote,
222 rename,
223 reserve,
224 reset,
225 resolve as resolve_cmd,
226 restore,
227 rm,
228 # retrograde, # midi — disabled until midi rollout
229 revert,
230 # rhythm, # midi — disabled until midi rollout
231 # scale_detect, # midi — disabled until midi rollout
232 semantic_cherry_pick,
233 semantic_test_coverage,
234 shard,
235 shortlog,
236 read,
237 sign,
238 snapshot_cmd,
239 shelf,
240 social as social_cmd,
241 stable,
242 status,
243 switch,
244 symbol_log,
245 symlog as symlog_cmd,
246 symbols,
247 label,
248 tag,
249 task_queue as task_queue_cmd,
250 # tempo, # midi — disabled until midi rollout
251 # tension, # midi — disabled until midi rollout
252 test_cmd,
253 # transpose, # midi — disabled until midi rollout
254 type_cmd,
255 velocity,
256 # velocity_normalize, # midi — disabled until midi rollout
257 # velocity_profile, # midi — disabled until midi rollout
258 verify,
259 # voice_leading, # midi — disabled until midi rollout
260 watch_coord,
261
262 workspace,
263 worktree,
264 )
265
266 from muse.cli.commands import (
267 apply_patch,
268 cat_object,
269 check_attr,
270 count_objects,
271 check_ignore,
272 check_ref_format,
273 commit_graph,
274 commit_tree,
275 domain_info,
276 for_each_ref,
277 format_patch,
278 hash_object,
279 ls_files,
280 ls_remote,
281 ls_tree,
282 merge_base,
283 merge_tree,
284 name_rev,
285 pack_objects,
286 patch_id,
287 read_commit,
288 read_snapshot,
289 rev_list,
290 rev_parse,
291 show_ref,
292 snapshot_diff,
293 sparse_checkout,
294 symbolic_ref,
295 unpack_objects,
296 update_ref,
297 verify_commit,
298 verify_object,
299 verify_pack,
300 verify_tag,
301 )
302
303 def _no_command(parser: argparse.ArgumentParser) -> None:
304 """Print help when no subcommand is provided."""
305 parser.print_help()
306 raise SystemExit(0)
307
308 class _MuseArgumentParser(argparse.ArgumentParser):
309 """ArgumentParser subclass that adds 'did you mean?' suggestions on error.
310
311 When the user mistypes a subcommand (e.g. ``muse comit``), argparse
312 normally prints only "argument COMMAND: invalid choice: 'comit'". This
313 subclass intercepts that error, checks for close matches among the
314 registered subcommand names, and appends a suggestion line so agents and
315 humans can self-correct without consulting the full ``--help`` output.
316
317 Example output::
318
319 muse: error: argument COMMAND: invalid choice: 'comit'
320 Did you mean: commit?
321 """
322
323 def error(self, message: str) -> None:
324 # Extract the unknown token from messages like:
325 # "argument COMMAND: invalid choice: 'comit' (choose from ...)"
326 unknown: str | None = None
327 if "invalid choice:" in message:
328 try:
329 start = message.index("'") + 1
330 end = message.index("'", start)
331 unknown = message[start:end]
332 except ValueError:
333 pass
334
335 if unknown is not None:
336 choices: list[str] = []
337 for action in self._actions:
338 if isinstance(action, argparse._SubParsersAction):
339 choices.extend(action.choices.keys())
340 suggestions = difflib.get_close_matches(unknown, choices, n=3, cutoff=0.6)
341 if suggestions:
342 message = f"{message}\n Did you mean: {', '.join(suggestions)}?"
343
344 super().error(message)
345
346 def main(argv: list[str] | None = None) -> None:
347 """Parse arguments and dispatch to the appropriate command handler.
348
349 Exit codes
350 ----------
351 0 Success — command completed normally.
352 1 Runtime error — e.g. ``-C DIR`` directory does not exist.
353 2 Argument error — unknown command, missing required argument, or a
354 namespace command (``code``/``midi``/``coord``) was invoked without a
355 subcommand. argparse prints the error before this function raises.
356
357 Flag processing order
358 ---------------------
359 1. ``--version`` / ``-V`` — print version and exit 0, no further parsing.
360 2. ``-C DIR`` — ``os.chdir(DIR)`` before dispatch; exit 1 on
361 ``OSError`` (non-existent or permission-denied).
362 3. Subcommand dispatch — ``args.func(args)`` called once.
363
364 No-subcommand behaviour
365 -----------------------
366 ``muse`` with no subcommand prints the help text and exits 0.
367 ``muse code`` / ``muse midi`` / ``muse coord`` with no subcommand exits 2
368 (argparse enforces ``subs.required = True``).
369
370 Calling from tests
371 ------------------
372 Pass an explicit ``argv`` list to avoid reading ``sys.argv``::
373
374 from tests.cli_test_helper import CliRunner
375 result = CliRunner().invoke(None, ["--version"])
376 assert "muse" in result.output
377 """
378 parser = _MuseArgumentParser(
379 prog="muse",
380 description="Muse — domain-agnostic version control for multidimensional state.",
381 formatter_class=argparse.RawDescriptionHelpFormatter,
382 )
383 parser.add_argument(
384 "--version", "-V", "-v",
385 action="store_true",
386 dest="show_version",
387 help="Print 'muse <version>' to stdout and exit 0. "
388 "Agents: parse with output.split()[1] for the bare version string.",
389 )
390 parser.add_argument(
391 "-C",
392 metavar="DIR",
393 dest="chdir",
394 default=None,
395 help="Change to DIR before running the command (DIR must exist).",
396 )
397
398 subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
399
400 # ``muse help`` — ergonomic alias for ``muse --help``.
401 # Agents and humans reach for ``help`` by reflex; honour it.
402 subparsers.add_parser("help", help="Show this help message and exit.")
403
404 # ------------------------------------------------------------------
405 # All top-level commands — alphabetical order.
406 # All top-level commands are interleaved alphabetically
407 # so that ``muse --help`` presents one sorted list.
408 # ------------------------------------------------------------------
409 agent_cmd.register(subparsers)
410 agent_config.register(subparsers)
411 annotate.register(subparsers)
412 apply.register(subparsers) # "apply"
413 apply_patch.register(subparsers) # "apply-patch"
414 archive.register(subparsers)
415 attributes.register(subparsers)
416 auth.register(subparsers)
417 bisect.register(subparsers)
418 core_blame.register(subparsers) # "blame"
419 branch.register(subparsers)
420 bridge.register(subparsers)
421 bundle.register(subparsers)
422 core_cat.register(subparsers) # "cat" — file-level
423 cat_object.register(subparsers) # "cat-object"
424 check.register(subparsers)
425 check_attr.register(subparsers) # "check-attr"
426 check_ignore.register(subparsers) # "check-ignore"
427 check_ref_format.register(subparsers) # "check-ref-format"
428 checkout.register(subparsers)
429 cherry_pick.register(subparsers) # "cherry-pick"
430 clean.register(subparsers)
431 clone.register(subparsers)
432
433 # ── code namespace (muse code …) ──────────────────────────────────
434 code_parser = subparsers.add_parser(
435 "code",
436 help="[Tier 3] Code domain semantic commands — symbol graph, call graph, and provenance.",
437 description="Code domain semantic commands.",
438 )
439 code_subs = code_parser.add_subparsers(dest="code_command", metavar="CODE_COMMAND")
440 code_subs.required = True
441
442 code_stage.register_add(code_subs) # "add"
443 age_cmd.register(code_subs) # "age"
444 api_surface.register(code_subs) # "api-surface"
445 blast_risk.register(code_subs) # "blast-risk"
446 blame.register(code_subs) # "blame"
447 breakage.register(code_subs) # "breakage"
448 cat.register(code_subs) # "cat"
449 checkout_symbol.register(code_subs) # "checkout-symbol"
450 clones.register(code_subs) # "clones"
451 code_check.register(code_subs) # "code-check"
452 code_query.register(code_subs) # "code-query"
453 codemap.register(code_subs) # "codemap"
454 compare.register(code_subs) # "compare"
455 contract.register(code_subs) # "contract"
456 coupling.register(code_subs) # "coupling"
457 coverage.register(code_subs) # "coverage"
458 dead.register(subparsers) # "dead" — register wraps in code namespace
459 deps.register(code_subs) # "deps"
460 detect_refactor.register(code_subs) # "detect-refactor"
461 docs_cmd.register(code_subs) # "docs"
462 entangle.register(code_subs) # "entangle"
463 find_symbol.register(code_subs) # "find-symbol"
464 gravity.register(code_subs) # "gravity"
465 grep.register(code_subs) # "grep"
466 hotspots.register(code_subs) # "hotspots"
467 impact.register(code_subs) # "impact"
468 index_rebuild.register(code_subs) # "index"
469 invariants.register(code_subs) # "invariants"
470 languages.register(code_subs) # "languages"
471 lineage.register(code_subs) # "lineage"
472 migrate.register(code_subs) # "migrate"
473 narrative.register(code_subs) # "narrative"
474 patch.register(code_subs) # "patch"
475 predict.register(code_subs) # "predict"
476 query.register(code_subs) # "query"
477 query_history.register(code_subs) # "query-history"
478 rename.register(code_subs) # "rename"
479 code_stage.register_reset(code_subs) # "reset"
480 semantic_cherry_pick.register(code_subs) # "semantic-cherry-pick"
481 semantic_test_coverage.register(code_subs) # "semantic-test-coverage"
482 stable.register(code_subs) # "stable"
483 symbol_log.register(code_subs) # "symbol-log"
484 symbols.register(code_subs) # "symbols"
485 test_cmd.register(code_subs) # "test"
486 type_cmd.register(code_subs) # "type"
487 velocity.register(code_subs) # "velocity"
488 register_namespace("code", code_subs.choices.keys())
489 # ─────────────────────────────────────────────────────────────────
490
491 commit.register(subparsers)
492 commit_graph.register(subparsers) # "commit-graph"
493 commit_tree.register(subparsers) # "commit-tree"
494 config_cmd.register(subparsers) # "config"
495 conflicts.register(subparsers)
496 content_grep.register(subparsers) # "content-grep"
497 count_objects.register(subparsers) # "count-objects"
498
499 # ── coord namespace (muse coord …) ───────────────────────────────
500 coord_parser = subparsers.add_parser(
501 "coord",
502 help="[Tier 3] Multi-agent coordination commands — reservations, intent, conflict forecasting.",
503 description="Multi-agent coordination commands.",
504 )
505 coord_subs = coord_parser.add_subparsers(dest="coord_command", metavar="COORD_COMMAND")
506 coord_subs.required = True
507
508 task_queue_cmd.register_cancel_task(coord_subs) # "cancel-task"
509 task_queue_cmd.register_claim(coord_subs) # "claim"
510 task_queue_cmd.register_complete(coord_subs) # "complete"
511 dag_cmd.register(coord_subs) # "dag"
512 task_queue_cmd.register_enqueue(coord_subs) # "enqueue"
513 task_queue_cmd.register_fail_task(coord_subs) # "fail-task"
514 forecast.register(coord_subs) # "forecast"
515 coord_gc.register(coord_subs) # "gc"
516 heartbeat_coord.register(coord_subs) # "heartbeat"
517 intent.register(coord_subs) # "intent"
518 list_coord.register(coord_subs) # "list"
519 plan_merge.register(coord_subs) # "plan-merge"
520 reconcile.register(coord_subs) # "reconcile"
521 release_coord.register(coord_subs) # "release"
522 reserve.register(coord_subs) # "reserve"
523 shard.register(coord_subs) # "shard"
524 coord_sync.register(coord_subs) # "sync"
525 task_queue_cmd.register_tasks(coord_subs) # "tasks"
526 watch_coord.register(coord_subs) # "watch"
527 # ─────────────────────────────────────────────────────────────────
528
529 describe.register(subparsers)
530 diff.register(subparsers)
531 domain_cmd.register(subparsers) # "domain"
532 domain_info.register(subparsers) # "domain-info"
533 domains.register(subparsers)
534 fetch.register(subparsers)
535 for_each_ref.register(subparsers) # "for-each-ref"
536 format_patch.register(subparsers) # "format-patch"
537 gc.register(subparsers)
538 hash_object.register(subparsers) # "hash-object"
539 hub.register(subparsers)
540 init.register(subparsers)
541 log.register(subparsers)
542 ls_files.register(subparsers) # "ls-files"
543 ls_remote.register(subparsers) # "ls-remote"
544 ls_tree.register(subparsers) # "ls-tree"
545 maintenance.register(subparsers) # "maintenance"
546 merge.register(subparsers)
547 migrate_cmd.register(subparsers) # "migrate"
548 merge_base.register(subparsers) # "merge-base"
549 merge_tree.register(subparsers) # "merge-tree"
550 mist_cmd.register(subparsers) # "mist"
551 mv.register(subparsers)
552 harmony.register(subparsers)
553 social_cmd.register(subparsers) # "social"
554
555 # ── midi namespace (muse midi …) — disabled until midi rollout ───
556 # midi_parser = subparsers.add_parser(
557 # "midi",
558 # help="[Tier 3] MIDI domain semantic commands.",
559 # description="MIDI domain semantic commands — analysis, transformation, and multi-agent.",
560 # )
561 # midi_subs = midi_parser.add_subparsers(dest="midi_command", metavar="MIDI_COMMAND")
562 # midi_subs.required = True
563 #
564 # agent_map.register(midi_subs) # "agent-map"
565 # arpeggiate.register(midi_subs) # "arpeggiate"
566 # cadence.register(midi_subs) # "cadence"
567 # midi_check.register(midi_subs) # "check"
568 # midi_compare.register(midi_subs) # "compare"
569 # contour.register(midi_subs) # "contour"
570 # density.register(midi_subs) # "density"
571 # find_phrase.register(midi_subs) # "find-phrase"
572 # harmony.register(midi_subs) # "harmony"
573 # humanize.register(midi_subs) # "humanize"
574 # instrumentation.register(midi_subs) # "instrumentation"
575 # invert.register(midi_subs) # "invert"
576 # mix.register(midi_subs) # "mix"
577 # motif_detect.register(midi_subs) # "motif"
578 # velocity_normalize.register(midi_subs) # "normalize"
579 # note_blame.register(midi_subs) # "note-blame"
580 # note_hotspots.register(midi_subs) # "note-hotspots"
581 # note_log.register(midi_subs) # "note-log"
582 # notes.register(midi_subs) # "notes"
583 # piano_roll.register(midi_subs) # "piano-roll"
584 # quantize.register(midi_subs) # "quantize"
585 # midi_query.register(midi_subs) # "query"
586 # retrograde.register(midi_subs) # "retrograde"
587 # rhythm.register(midi_subs) # "rhythm"
588 # scale_detect.register(midi_subs) # "scale"
589 # midi_shard.register(midi_subs) # "shard"
590 # tempo.register(midi_subs) # "tempo"
591 # tension.register(midi_subs) # "tension"
592 # transpose.register(midi_subs) # "transpose"
593 # velocity_profile.register(midi_subs) # "velocity-profile"
594 # voice_leading.register(midi_subs) # "voice-leading"
595 # ─────────────────────────────────────────────────────────────────
596
597 name_rev.register(subparsers) # "name-rev"
598 pack_objects.register(subparsers) # "pack-objects"
599 patch_id.register(subparsers) # "patch-id"
600 path_cmd.register(subparsers) # "path"
601 prune.register(subparsers)
602 pull.register(subparsers)
603 push.register(subparsers)
604 range_diff.register(subparsers) # "range-diff"
605 rebase.register(subparsers)
606 read_commit.register(subparsers) # "read-commit"
607 read_snapshot.register(subparsers) # "read-snapshot"
608 reflog.register(subparsers)
609 release_cmd.register(subparsers) # "release"
610 remote.register(subparsers)
611 reset.register(subparsers)
612 resolve_cmd.register(subparsers) # "resolve"
613 restore.register(subparsers)
614 rm.register(subparsers)
615 revert.register(subparsers)
616 rev_list.register(subparsers) # "rev-list"
617 rev_parse.register(subparsers) # "rev-parse"
618 shortlog.register(subparsers)
619 read.register(subparsers) # "read"
620 show_ref.register(subparsers) # "show-ref"
621 sign.register(subparsers) # "sign"
622 snapshot_cmd.register(subparsers) # "snapshot"
623 snapshot_diff.register(subparsers) # "snapshot-diff"
624 sparse_checkout.register(subparsers) # "sparse-checkout"
625 shelf.register(subparsers)
626 status.register(subparsers)
627 switch.register(subparsers)
628 symlog_cmd.register(subparsers) # "symlog"
629 symbolic_ref.register(subparsers) # "symbolic-ref"
630 label.register(subparsers)
631 tag.register(subparsers)
632 trust.register(subparsers)
633 unpack_objects.register(subparsers) # "unpack-objects"
634 update_ref.register(subparsers) # "update-ref"
635 verify.register(subparsers)
636 verify_commit.register(subparsers) # "verify-commit"
637 verify_object.register(subparsers) # "verify-object"
638 verify_pack.register(subparsers) # "verify-pack"
639 verify_tag.register(subparsers) # "verify-tag"
640 workspace.register(subparsers)
641 worktree.register(subparsers)
642
643 # ------------------------------------------------------------------
644 # Parse and dispatch
645 # ------------------------------------------------------------------
646 args = parser.parse_args(argv)
647
648 if args.show_version:
649 from muse._version import __version__
650 # Output format is stable: exactly "muse <version>\n" on stdout, exit 0.
651 # Agents and scripts can parse the version without --json:
652 #
653 # version = subprocess.check_output(["muse", "--version"]).decode().split()[1]
654 #
655 # The token at index 1 (space-split) is always the bare semver string.
656 # This format will not change without a major-version bump.
657 print(f"muse {__version__}")
658 raise SystemExit(0)
659
660 if args.chdir is not None:
661 from muse.core.validation import sanitize_display
662 try:
663 os.chdir(args.chdir)
664 except OSError as exc:
665 safe_dir = sanitize_display(args.chdir)
666 print(f"muse: cannot change to directory '{safe_dir}': {exc}", file=sys.stderr)
667 raise SystemExit(1)
668
669 if args.command is None or args.command == "help":
670 _no_command(parser)
671 return
672
673 if not hasattr(args, "func"):
674 # A namespace command (midi/code/coord) was given without a subcommand.
675 # argparse already printed an error above via subs.required = True.
676 raise SystemExit(2)
677
678 args.func(args)
679
680 if __name__ == "__main__":
681 main()
File History 1 commit
sha256:24d9cb81ac1dd071e5d98862c740ad101180588361b4f96a790af79a99aed901 docs: architectural plan for unified-store snapshot-content… Sonnet 5 10 days ago