gabriel / muse public
config_cmd.py python
205 lines 5.5 KB
80353726 feat: muse auth + hub + config — paradigm-level identity architecture w… Gabriel Cardona <cgcardona@gmail.com> 4d ago
1 """muse config — local repository configuration.
2
3 Provides structured, typed read/write access to ``.muse/config.toml``.
4 For hub credentials, use ``muse auth``. For remote connections, use
5 ``muse remote``.
6
7 Settable namespaces
8 --------------------
9 - ``user.name`` — display name (human or agent handle)
10 - ``user.email`` — contact email
11 - ``user.type`` — ``"human"`` or ``"agent"``
12 - ``hub.url`` — hub fabric URL (alias for ``muse hub connect``)
13 - ``domain.*`` — domain-specific keys; read by the active plugin
14
15 Blocked via ``muse config set``
16 ---------------------------------
17 - ``auth.*`` — use ``muse auth login``
18 - ``remotes.*`` — use ``muse remote add/remove``
19
20 Output format
21 -------------
22 ``muse config show`` emits TOML by default (human-readable). Pass
23 ``--json`` for machine-readable output — no credentials are ever included.
24
25 Examples
26 --------
27 ::
28
29 muse config show
30 muse config show --json
31 muse config get user.name
32 muse config set user.name "Alice"
33 muse config set user.type agent
34 muse config set domain.ticks_per_beat 480
35 muse config edit
36 """
37
38 from __future__ import annotations
39
40 import json
41 import logging
42 import os
43 import subprocess
44 import sys
45
46 import typer
47
48 from muse.cli.config import (
49 config_as_dict,
50 config_path_for_editor,
51 get_config_value,
52 set_config_value,
53 )
54 from muse.core.errors import ExitCode
55 from muse.core.repo import find_repo_root
56
57 logger = logging.getLogger(__name__)
58
59 app = typer.Typer(no_args_is_help=True)
60
61
62 # ---------------------------------------------------------------------------
63 # Commands
64 # ---------------------------------------------------------------------------
65
66
67 @app.command()
68 def show(
69 json_output: bool = typer.Option(
70 False,
71 "--json",
72 help="Emit JSON instead of TOML.",
73 ),
74 ) -> None:
75 """Display the current repository configuration.
76
77 Output is TOML by default. Use ``--json`` for agent-friendly output.
78 Credentials are never included regardless of format.
79 """
80 root = find_repo_root()
81
82 data = config_as_dict(root)
83
84 if json_output:
85 typer.echo(json.dumps(data, indent=2))
86 return
87
88 # Render as TOML-like display
89 if not data:
90 typer.echo("# No configuration set.")
91 return
92
93 user = data.get("user")
94 if user:
95 typer.echo("[user]")
96 for key, val in sorted(user.items()):
97 typer.echo(f'{key} = "{val}"')
98 typer.echo("")
99
100 hub = data.get("hub")
101 if hub:
102 typer.echo("[hub]")
103 for key, val in sorted(hub.items()):
104 typer.echo(f'{key} = "{val}"')
105 typer.echo("")
106
107 remotes = data.get("remotes")
108 if remotes:
109 for remote_name, remote_url in sorted(remotes.items()):
110 typer.echo(f"[remotes.{remote_name}]")
111 typer.echo(f'url = "{remote_url}"')
112 typer.echo("")
113
114 domain = data.get("domain")
115 if domain:
116 typer.echo("[domain]")
117 for key, val in sorted(domain.items()):
118 typer.echo(f'{key} = "{val}"')
119 typer.echo("")
120
121
122 @app.command()
123 def get(
124 key: str = typer.Argument(
125 ...,
126 metavar="KEY",
127 help="Dotted key to read (e.g. user.name, hub.url, domain.ticks_per_beat).",
128 ),
129 ) -> None:
130 """Print the value of a single config key.
131
132 Exits non-zero when the key is not set, so agents can branch::
133
134 VALUE=$(muse config get user.type) || echo "not set"
135 """
136 root = find_repo_root()
137 value = get_config_value(key, root)
138
139 if value is None:
140 typer.echo(f"# {key} is not set", err=True)
141 raise typer.Exit(code=ExitCode.USER_ERROR)
142
143 typer.echo(value)
144
145
146 @app.command()
147 def set( # noqa: A001
148 key: str = typer.Argument(
149 ...,
150 metavar="KEY",
151 help="Dotted key to set (e.g. user.name, domain.ticks_per_beat).",
152 ),
153 value: str = typer.Argument(
154 ...,
155 metavar="VALUE",
156 help="New value (always stored as a string).",
157 ),
158 ) -> None:
159 """Set a config value by dotted key.
160
161 Examples::
162
163 muse config set user.name "Alice"
164 muse config set user.type agent
165 muse config set hub.url https://musehub.ai
166 muse config set domain.ticks_per_beat 480
167
168 For credentials, use ``muse auth login``.
169 For remotes, use ``muse remote add``.
170 """
171 root = find_repo_root()
172 try:
173 set_config_value(key, value, root)
174 except ValueError as exc:
175 typer.echo(f"❌ {exc}")
176 raise typer.Exit(code=ExitCode.USER_ERROR) from exc
177
178 typer.echo(f"✅ {key} = {value!r}")
179
180
181 @app.command()
182 def edit() -> None:
183 """Open ``.muse/config.toml`` in ``$EDITOR`` or ``$VISUAL``.
184
185 Falls back to ``vi`` when neither environment variable is set.
186 """
187 root = find_repo_root()
188 if root is None:
189 typer.echo("❌ Not inside a Muse repository.")
190 raise typer.Exit(code=ExitCode.REPO_NOT_FOUND)
191
192 config_path = config_path_for_editor(root)
193 if not config_path.is_file():
194 typer.echo(f"❌ Config file not found: {config_path}")
195 raise typer.Exit(code=ExitCode.USER_ERROR)
196
197 editor = os.environ.get("VISUAL") or os.environ.get("EDITOR") or "vi"
198 try:
199 subprocess.run([editor, str(config_path)], check=True)
200 except FileNotFoundError:
201 typer.echo(f"❌ Editor not found: {editor!r}")
202 raise typer.Exit(code=ExitCode.USER_ERROR)
203 except subprocess.CalledProcessError as exc:
204 typer.echo(f"❌ Editor exited with code {exc.returncode}")
205 raise typer.Exit(code=ExitCode.USER_ERROR)