mad-cli 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mad_cli/__init__.py +3 -0
- mad_cli/__main__.py +6 -0
- mad_cli/app.py +77 -0
- mad_cli/commands/__init__.py +5 -0
- mad_cli/commands/_adapt.py +41 -0
- mad_cli/commands/_common.py +12 -0
- mad_cli/commands/config.py +94 -0
- mad_cli/commands/install.py +504 -0
- mad_cli/commands/instances.py +102 -0
- mad_cli/commands/keys.py +126 -0
- mad_cli/commands/lifecycle.py +69 -0
- mad_cli/commands/profiles.py +238 -0
- mad_cli/commands/service.py +220 -0
- mad_cli/commands/versions.py +61 -0
- mad_cli/core/__init__.py +4 -0
- mad_cli/core/claude_creds.py +31 -0
- mad_cli/core/compose.py +145 -0
- mad_cli/core/docker_check.py +89 -0
- mad_cli/core/envfile.py +140 -0
- mad_cli/core/instance.py +110 -0
- mad_cli/core/keyspec.py +98 -0
- mad_cli/core/paths.py +40 -0
- mad_cli/core/profiles.py +93 -0
- mad_cli/core/pypi.py +29 -0
- mad_cli/core/templates.py +91 -0
- mad_cli/core/usecases/__init__.py +11 -0
- mad_cli/core/usecases/adopt.py +55 -0
- mad_cli/core/usecases/configvals.py +94 -0
- mad_cli/core/usecases/errors.py +57 -0
- mad_cli/core/usecases/install.py +263 -0
- mad_cli/core/usecases/instances.py +156 -0
- mad_cli/core/usecases/keys.py +169 -0
- mad_cli/core/usecases/lifecycle.py +76 -0
- mad_cli/core/usecases/service.py +269 -0
- mad_cli/core/usecases/versions.py +126 -0
- mad_cli/py.typed +0 -0
- mad_cli/server/__init__.py +13 -0
- mad_cli/server/app.py +260 -0
- mad_cli/server/auth.py +41 -0
- mad_cli/server/models.py +156 -0
- mad_cli/templates/Dockerfile.tmpl +66 -0
- mad_cli/templates/__init__.py +6 -0
- mad_cli/templates/com.mad-core.mad-cli.plist.tmpl +28 -0
- mad_cli/templates/compose.yml.tmpl +29 -0
- mad_cli/templates/entrypoint.sh.tmpl +11 -0
- mad_cli/templates/mad-cli.service.tmpl +15 -0
- mad_cli/ui/__init__.py +5 -0
- mad_cli/ui/console.py +65 -0
- mad_cli/ui/prompts.py +83 -0
- mad_cli-0.4.0.dist-info/METADATA +167 -0
- mad_cli-0.4.0.dist-info/RECORD +54 -0
- mad_cli-0.4.0.dist-info/WHEEL +4 -0
- mad_cli-0.4.0.dist-info/entry_points.txt +2 -0
- mad_cli-0.4.0.dist-info/licenses/LICENSE +21 -0
mad_cli/__init__.py
ADDED
mad_cli/__main__.py
ADDED
mad_cli/app.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Typer application wiring and the ``mad`` console-script entry point."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
|
|
5
|
+
from mad_cli import __version__
|
|
6
|
+
from mad_cli.commands import config as config_cmd
|
|
7
|
+
from mad_cli.commands import install as install_cmd
|
|
8
|
+
from mad_cli.commands import instances as instances_cmd
|
|
9
|
+
from mad_cli.commands import keys as keys_cmd
|
|
10
|
+
from mad_cli.commands import lifecycle as lifecycle_cmd
|
|
11
|
+
from mad_cli.commands import profiles as profiles_cmd
|
|
12
|
+
from mad_cli.commands import service as service_cmd
|
|
13
|
+
from mad_cli.commands import versions as versions_cmd
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(
|
|
16
|
+
no_args_is_help=True,
|
|
17
|
+
add_completion=False,
|
|
18
|
+
help="Mad operator CLI — install and manage mad-edge containers.",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _version_callback(value: bool) -> None:
|
|
23
|
+
if value:
|
|
24
|
+
typer.echo(__version__)
|
|
25
|
+
raise typer.Exit()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.callback()
|
|
29
|
+
def _root(
|
|
30
|
+
version: bool = typer.Option(
|
|
31
|
+
False,
|
|
32
|
+
"--version",
|
|
33
|
+
callback=_version_callback,
|
|
34
|
+
is_eager=True,
|
|
35
|
+
help="Show the mad-cli version and exit.",
|
|
36
|
+
),
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Mad operator CLI — install and manage mad-edge containers."""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ── install ──────────────────────────────────────────────────────────────────
|
|
42
|
+
app.command("install")(install_cmd.install)
|
|
43
|
+
|
|
44
|
+
# ── lifecycle ────────────────────────────────────────────────────────────────
|
|
45
|
+
app.command("start")(lifecycle_cmd.start)
|
|
46
|
+
app.command("stop")(lifecycle_cmd.stop)
|
|
47
|
+
app.command("restart")(lifecycle_cmd.restart)
|
|
48
|
+
app.command("status")(lifecycle_cmd.status)
|
|
49
|
+
app.command("logs")(lifecycle_cmd.logs)
|
|
50
|
+
app.command("shell")(lifecycle_cmd.shell)
|
|
51
|
+
|
|
52
|
+
# ── inventory ────────────────────────────────────────────────────────────────
|
|
53
|
+
app.command("list")(instances_cmd.list_)
|
|
54
|
+
app.command("info")(instances_cmd.info_cmd)
|
|
55
|
+
app.command("adopt")(instances_cmd.adopt)
|
|
56
|
+
|
|
57
|
+
# ── versions ─────────────────────────────────────────────────────────────────
|
|
58
|
+
app.command("versions")(versions_cmd.versions)
|
|
59
|
+
app.command("update")(versions_cmd.update)
|
|
60
|
+
|
|
61
|
+
# ── keys & config ─────────────────────────────────────────────────────────────
|
|
62
|
+
app.add_typer(keys_cmd.keys_app, name="keys")
|
|
63
|
+
app.add_typer(config_cmd.config_app, name="config")
|
|
64
|
+
app.add_typer(profiles_cmd.profiles_app, name="profiles")
|
|
65
|
+
|
|
66
|
+
# ── service mode (HTTP API) ───────────────────────────────────────────────────
|
|
67
|
+
app.command("serve")(service_cmd.serve)
|
|
68
|
+
app.add_typer(service_cmd.service_app, name="service")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def main() -> None:
|
|
72
|
+
"""Console-script entry point (``mad``)."""
|
|
73
|
+
app()
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__": # pragma: no cover
|
|
77
|
+
main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Shared CLI adapter helpers: instance resolution and use-case error mapping.
|
|
2
|
+
|
|
3
|
+
The command modules are thin adapters over :mod:`mad_cli.core.usecases`; this maps
|
|
4
|
+
the framework-free :class:`~mad_cli.core.usecases.errors.UseCaseError` vocabulary
|
|
5
|
+
onto the CLI idiom (an ``error(...)`` line plus ``typer.Exit(1)``) with the
|
|
6
|
+
operator-facing hints (``mad install`` / ``mad list``).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import NoReturn
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from mad_cli.core.instance import Instance
|
|
16
|
+
from mad_cli.core.usecases import instances as uc_instances
|
|
17
|
+
from mad_cli.core.usecases.errors import AmbiguousInstanceError, NotFoundError, UseCaseError
|
|
18
|
+
from mad_cli.ui.console import error
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def die(msg: str) -> NoReturn:
|
|
22
|
+
"""Print an error and exit non-zero."""
|
|
23
|
+
error(msg)
|
|
24
|
+
raise typer.Exit(1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def fail(exc: UseCaseError) -> NoReturn:
|
|
28
|
+
"""Render a use-case failure verbatim and exit non-zero."""
|
|
29
|
+
die(str(exc))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def resolve_or_die(name: str | None) -> Instance:
|
|
33
|
+
"""Resolve ``name`` (or the sole instance) or exit with an actionable hint."""
|
|
34
|
+
try:
|
|
35
|
+
return uc_instances.resolve_instance(name)
|
|
36
|
+
except NotFoundError:
|
|
37
|
+
if name is not None:
|
|
38
|
+
die(f"Instance {name!r} not found. Run `mad list` to see available instances.")
|
|
39
|
+
die("No instances found. Run `mad install` first.")
|
|
40
|
+
except AmbiguousInstanceError as exc:
|
|
41
|
+
die(str(exc))
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Small presentation helpers shared across commands.
|
|
2
|
+
|
|
3
|
+
Secret detection now lives in the framework-free engine
|
|
4
|
+
(:func:`mad_cli.core.keyspec.is_secret_key`) so the HTTP surface and the CLI
|
|
5
|
+
share one masking rule; it is re-exported here for the command modules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from mad_cli.core.keyspec import is_secret_key
|
|
11
|
+
|
|
12
|
+
__all__ = ["is_secret_key"]
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""``mad config get|set|unset`` — read and edit an instance's ``.env`` values.
|
|
2
|
+
|
|
3
|
+
Thin adapter over :mod:`mad_cli.core.usecases.configvals` — the general-purpose
|
|
4
|
+
``.env`` editor (``mad keys`` is the credential-aware front end). Secret-looking
|
|
5
|
+
values are masked on read unless ``--reveal`` is passed, and a handful of known
|
|
6
|
+
keys are validated on write.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.table import Table
|
|
13
|
+
|
|
14
|
+
from mad_cli.commands._adapt import fail, resolve_or_die
|
|
15
|
+
from mad_cli.core.instance import Instance
|
|
16
|
+
from mad_cli.core.usecases import configvals as uc
|
|
17
|
+
from mad_cli.core.usecases.errors import NotFoundError, ValidationError
|
|
18
|
+
from mad_cli.ui.console import console, error, info, ok, warn
|
|
19
|
+
|
|
20
|
+
config_app = typer.Typer(
|
|
21
|
+
no_args_is_help=True,
|
|
22
|
+
add_completion=False,
|
|
23
|
+
help="Read and edit an instance's .env configuration.",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_INSTANCE_OPTION = typer.Option(
|
|
27
|
+
None, "--instance", "-i", help="Instance name (optional when exactly one instance exists)."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _restart_hint(instance: Instance) -> None:
|
|
32
|
+
info(f"Restart the instance to apply: mad restart {instance.name}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@config_app.command("get")
|
|
36
|
+
def get(
|
|
37
|
+
key: str | None = typer.Argument(None, help="Env key to read. Omit to list every value."),
|
|
38
|
+
instance: str | None = _INSTANCE_OPTION,
|
|
39
|
+
reveal: bool = typer.Option(False, "--reveal", help="Show secret values in full."),
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Print one ``.env`` value, or the whole file (secrets masked)."""
|
|
42
|
+
inst = resolve_or_die(instance)
|
|
43
|
+
|
|
44
|
+
if key is None:
|
|
45
|
+
table = Table(title=f".env — {inst.name}")
|
|
46
|
+
table.add_column("Key", style="bold", no_wrap=True)
|
|
47
|
+
table.add_column("Value")
|
|
48
|
+
for item in uc.list_config(inst):
|
|
49
|
+
table.add_row(item.key, item.display(reveal=reveal))
|
|
50
|
+
console.print(table)
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
item = uc.get_config(inst, key)
|
|
55
|
+
except NotFoundError:
|
|
56
|
+
error(f"{key} is not set on {inst.name}.")
|
|
57
|
+
raise typer.Exit(1) from None
|
|
58
|
+
console.print(item.display(reveal=reveal), markup=False, highlight=False)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@config_app.command("set")
|
|
62
|
+
def set_value(
|
|
63
|
+
key: str = typer.Argument(..., help="Env key to write."),
|
|
64
|
+
value: str = typer.Argument(..., help="Value to store."),
|
|
65
|
+
instance: str | None = _INSTANCE_OPTION,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Set an ``.env`` value, validating a handful of known keys."""
|
|
68
|
+
inst = resolve_or_die(instance)
|
|
69
|
+
try:
|
|
70
|
+
item, compose_baked = uc.set_config(inst, key, value)
|
|
71
|
+
except ValidationError as exc:
|
|
72
|
+
fail(exc)
|
|
73
|
+
|
|
74
|
+
ok(f"Set {key} = {item.display()} on {inst.name}.")
|
|
75
|
+
if compose_baked:
|
|
76
|
+
warn(
|
|
77
|
+
f"{key} was baked into compose.yml at install time. This updates .env only; "
|
|
78
|
+
"a port/bind change needs the instance regenerated (v0.3+) to take effect."
|
|
79
|
+
)
|
|
80
|
+
_restart_hint(inst)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@config_app.command("unset")
|
|
84
|
+
def unset(
|
|
85
|
+
key: str = typer.Argument(..., help="Env key to remove."),
|
|
86
|
+
instance: str | None = _INSTANCE_OPTION,
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Remove a key from the instance's ``.env``."""
|
|
89
|
+
inst = resolve_or_die(instance)
|
|
90
|
+
if not uc.unset_config(inst, key):
|
|
91
|
+
warn(f"{key} is not set on {inst.name}; nothing to remove.")
|
|
92
|
+
return
|
|
93
|
+
ok(f"Unset {key} on {inst.name}.")
|
|
94
|
+
_restart_hint(inst)
|