cgcardona / muse public
remote.py python
188 lines 6.1 KB
12901c5a Initial extraction from tellurstori/maestro cgcardona <gabriel@tellurstori.com> 4d ago
1 """muse remote — manage remote Muse Hub connections.
2
3 Subcommands:
4
5 muse remote add <name> <url>
6 Write ``[remotes.<name>] url = "<url>"`` to ``.muse/config.toml``.
7 Creates the config file if it does not exist.
8
9 muse remote remove <name>
10 Remove a configured remote and all its refs/remotes/<name>/ tracking refs.
11
12 muse remote rename <old> <new>
13 Rename a remote in config and move its tracking ref paths.
14
15 muse remote set-url <name> <url>
16 Update the URL of an existing remote without touching tracking refs.
17
18 muse remote -v / --verbose
19 Print all configured remotes with their URLs.
20 Token values in [auth] are masked — this command is safe to run in CI.
21
22 Exit codes follow the Muse CLI contract (``errors.ExitCode``):
23 0 — success
24 1 — user error (bad arguments)
25 2 — not a Muse repository
26 """
27 from __future__ import annotations
28
29 import logging
30
31 import typer
32
33 from maestro.muse_cli._repo import require_repo
34 from maestro.muse_cli.config import get_remote, list_remotes, remove_remote, rename_remote, set_remote
35 from maestro.muse_cli.errors import ExitCode
36
37 logger = logging.getLogger(__name__)
38
39 app = typer.Typer(invoke_without_command=True)
40
41
42 @app.callback(invoke_without_command=True)
43 def remote(
44 ctx: typer.Context,
45 verbose: bool = typer.Option(
46 False,
47 "-v",
48 "--verbose",
49 help="Print all configured remotes and their URLs.",
50 is_eager=False,
51 ),
52 ) -> None:
53 """Manage remote Muse Hub connections.
54
55 Run ``muse remote add <name> <url>`` to register a remote, then
56 ``muse push`` / ``muse pull`` to sync with it.
57 """
58 root = require_repo()
59
60 # When invoked as `muse remote -v` (no subcommand), show remotes list.
61 if ctx.invoked_subcommand is None:
62 remotes = list_remotes(root)
63 if not remotes:
64 typer.echo("(no remotes configured — run `muse remote add <name> <url>`)")
65 return
66 for r in remotes:
67 typer.echo(f"{r['name']}\t{r['url']}")
68
69
70 @app.command("add")
71 def remote_add(
72 name: str = typer.Argument(..., help="Remote name (e.g. 'origin')."),
73 url: str = typer.Argument(
74 ...,
75 help="Remote URL (e.g. 'https://hub.example.com/musehub/repos/<repo-id>').",
76 ),
77 ) -> None:
78 """Register a named remote Hub URL in .muse/config.toml.
79
80 Example::
81
82 muse remote add origin https://story.audio/musehub/repos/my-repo-id
83
84 After adding a remote, use ``muse push`` and ``muse pull`` to sync.
85 """
86 root = require_repo()
87
88 if not name.strip():
89 typer.echo("❌ Remote name cannot be empty.")
90 raise typer.Exit(code=int(ExitCode.USER_ERROR))
91
92 if not url.strip().startswith(("http://", "https://")):
93 typer.echo(f"❌ URL must start with http:// or https:// — got: {url!r}")
94 raise typer.Exit(code=int(ExitCode.USER_ERROR))
95
96 set_remote(name.strip(), url.strip(), root)
97 typer.echo(f"✅ Remote '{name}' set to {url}")
98 logger.info("✅ muse remote add %r %s", name, url)
99
100
101 @app.command("remove")
102 def remote_remove(
103 name: str = typer.Argument(..., help="Remote name to remove (e.g. 'origin')."),
104 ) -> None:
105 """Remove a configured remote and all its local tracking refs.
106
107 Deletes ``[remotes.<name>]`` from ``.muse/config.toml`` and removes the
108 ``.muse/remotes/<name>/`` directory tree. Errors if the remote does not
109 exist.
110
111 Example::
112
113 muse remote remove origin
114 """
115 root = require_repo()
116
117 try:
118 remove_remote(name.strip(), root)
119 except KeyError:
120 typer.echo(f"❌ Remote '{name}' does not exist.")
121 raise typer.Exit(code=int(ExitCode.USER_ERROR)) from None
122
123 typer.echo(f"✅ Remote '{name}' removed.")
124 logger.info("✅ muse remote remove %r", name)
125
126
127 @app.command("rename")
128 def remote_rename(
129 old_name: str = typer.Argument(..., help="Current remote name."),
130 new_name: str = typer.Argument(..., help="New remote name."),
131 ) -> None:
132 """Rename a remote in config and move its tracking ref paths.
133
134 Updates ``[remotes.<old>]`` → ``[remotes.<new>]`` in ``.muse/config.toml``
135 and moves ``.muse/remotes/<old>/`` → ``.muse/remotes/<new>/``. Errors if
136 the old remote does not exist or the new name is already taken.
137
138 Example::
139
140 muse remote rename origin upstream
141 """
142 root = require_repo()
143
144 if not new_name.strip():
145 typer.echo("❌ New remote name cannot be empty.")
146 raise typer.Exit(code=int(ExitCode.USER_ERROR))
147
148 try:
149 rename_remote(old_name.strip(), new_name.strip(), root)
150 except KeyError:
151 typer.echo(f"❌ Remote '{old_name}' does not exist.")
152 raise typer.Exit(code=int(ExitCode.USER_ERROR)) from None
153 except ValueError:
154 typer.echo(f"❌ Remote '{new_name}' already exists.")
155 raise typer.Exit(code=int(ExitCode.USER_ERROR)) from None
156
157 typer.echo(f"✅ Remote '{old_name}' renamed to '{new_name}'.")
158 logger.info("✅ muse remote rename %r → %r", old_name, new_name)
159
160
161 @app.command("set-url")
162 def remote_set_url(
163 name: str = typer.Argument(..., help="Remote name (e.g. 'origin')."),
164 url: str = typer.Argument(..., help="New URL for the remote."),
165 ) -> None:
166 """Update the URL of an existing remote without touching tracking refs.
167
168 Updates ``[remotes.<name>] url`` in ``.muse/config.toml``. Unlike
169 ``muse remote add``, this command errors if the remote does not already
170 exist — use ``add`` for first-time registration.
171
172 Example::
173
174 muse remote set-url origin https://new-hub.example.com/musehub/repos/my-repo
175 """
176 root = require_repo()
177
178 if not url.strip().startswith(("http://", "https://")):
179 typer.echo(f"❌ URL must start with http:// or https:// — got: {url!r}")
180 raise typer.Exit(code=int(ExitCode.USER_ERROR))
181
182 if get_remote(name.strip(), root) is None:
183 typer.echo(f"❌ Remote '{name}' does not exist. Use `muse remote add` to create it.")
184 raise typer.Exit(code=int(ExitCode.USER_ERROR))
185
186 set_remote(name.strip(), url.strip(), root)
187 typer.echo(f"✅ Remote '{name}' URL changed to {url}")
188 logger.info("✅ muse remote set-url %r %s", name, url)